]> git.lizzy.rs Git - rust.git/blob - src/tools/rustdoc-js/tester.js
Auto merge of #95356 - coolreader18:exitstatus-exit-method, r=<try>
[rust.git] / src / tools / rustdoc-js / tester.js
1 const fs = require('fs');
2 const path = require('path');
3
4 function getNextStep(content, pos, stop) {
5     while (pos < content.length && content[pos] !== stop &&
6            (content[pos] === ' ' || content[pos] === '\t' || content[pos] === '\n')) {
7         pos += 1;
8     }
9     if (pos >= content.length) {
10         return null;
11     }
12     if (content[pos] !== stop) {
13         return pos * -1;
14     }
15     return pos;
16 }
17
18 // Stupid function extractor based on indent. Doesn't support block
19 // comments. If someone puts a ' or an " in a block comment this
20 // will blow up. Template strings are not tested and might also be
21 // broken.
22 function extractFunction(content, functionName) {
23     var level = 0;
24     var splitter = "function " + functionName + "(";
25     var stop;
26     var pos, start;
27
28     while (true) {
29         start = content.indexOf(splitter);
30         if (start === -1) {
31             break;
32         }
33         pos = start;
34         while (pos < content.length && content[pos] !== ')') {
35             pos += 1;
36         }
37         if (pos >= content.length) {
38             break;
39         }
40         pos = getNextStep(content, pos + 1, '{');
41         if (pos === null) {
42             break;
43         } else if (pos < 0) {
44             content = content.slice(-pos);
45             continue;
46         }
47         while (pos < content.length) {
48             // Eat single-line comments
49             if (content[pos] === '/' && pos > 0 && content[pos - 1] === '/') {
50                 do {
51                     pos += 1;
52                 } while (pos < content.length && content[pos] !== '\n');
53
54             // Eat multiline comment.
55             } else if (content[pos] === '*' && pos > 0 && content[pos - 1] === '/') {
56                 do {
57                     pos += 1;
58                 } while (pos < content.length && content[pos] !== '/' && content[pos - 1] !== '*');
59
60             // Eat quoted strings
61             } else if ((content[pos] === '"' || content[pos] === "'" || content[pos] === "`") &&
62                        (pos === 0 || content[pos - 1] !== '/')) {
63                 stop = content[pos];
64                 do {
65                     if (content[pos] === '\\') {
66                         pos += 1;
67                     }
68                     pos += 1;
69                 } while (pos < content.length && content[pos] !== stop);
70
71             // Otherwise, check for block level.
72             } else if (content[pos] === '{') {
73                 level += 1;
74             } else if (content[pos] === '}') {
75                 level -= 1;
76                 if (level === 0) {
77                     return content.slice(start, pos + 1);
78                 }
79             }
80             pos += 1;
81         }
82         content = content.slice(start + 1);
83     }
84     return null;
85 }
86
87 // Stupid function extractor for array.
88 function extractArrayVariable(content, arrayName, kind) {
89     if (typeof kind === "undefined") {
90         kind = "let ";
91     }
92     var splitter = kind + arrayName;
93     while (true) {
94         var start = content.indexOf(splitter);
95         if (start === -1) {
96             break;
97         }
98         var pos = getNextStep(content, start, '=');
99         if (pos === null) {
100             break;
101         } else if (pos < 0) {
102             content = content.slice(-pos);
103             continue;
104         }
105         pos = getNextStep(content, pos, '[');
106         if (pos === null) {
107             break;
108         } else if (pos < 0) {
109             content = content.slice(-pos);
110             continue;
111         }
112         while (pos < content.length) {
113             if (content[pos] === '"' || content[pos] === "'") {
114                 var stop = content[pos];
115                 do {
116                     if (content[pos] === '\\') {
117                         pos += 2;
118                     } else {
119                         pos += 1;
120                     }
121                 } while (pos < content.length &&
122                          (content[pos] !== stop || content[pos - 1] === '\\'));
123             } else if (content[pos] === ']' &&
124                        pos + 1 < content.length &&
125                        content[pos + 1] === ';') {
126                 return content.slice(start, pos + 2);
127             }
128             pos += 1;
129         }
130         content = content.slice(start + 1);
131     }
132     if (kind === "let ") {
133         return extractArrayVariable(content, arrayName, "const ");
134     }
135     return null;
136 }
137
138 // Stupid function extractor for variable.
139 function extractVariable(content, varName, kind) {
140     if (typeof kind === "undefined") {
141         kind = "let ";
142     }
143     var splitter = kind + varName;
144     while (true) {
145         var start = content.indexOf(splitter);
146         if (start === -1) {
147             break;
148         }
149         var pos = getNextStep(content, start, '=');
150         if (pos === null) {
151             break;
152         } else if (pos < 0) {
153             content = content.slice(-pos);
154             continue;
155         }
156         while (pos < content.length) {
157             if (content[pos] === '"' || content[pos] === "'") {
158                 var stop = content[pos];
159                 do {
160                     if (content[pos] === '\\') {
161                         pos += 2;
162                     } else {
163                         pos += 1;
164                     }
165                 } while (pos < content.length &&
166                          (content[pos] !== stop || content[pos - 1] === '\\'));
167             } else if (content[pos] === ';' || content[pos] === ',') {
168                 return content.slice(start, pos + 1);
169             }
170             pos += 1;
171         }
172         content = content.slice(start + 1);
173     }
174     if (kind === "let ") {
175         return extractVariable(content, varName, "const ");
176     }
177     return null;
178 }
179
180 function loadContent(content) {
181     var Module = module.constructor;
182     var m = new Module();
183     m._compile(content, "tmp.js");
184     m.exports.ignore_order = content.indexOf("\n// ignore-order\n") !== -1 ||
185         content.startsWith("// ignore-order\n");
186     m.exports.exact_check = content.indexOf("\n// exact-check\n") !== -1 ||
187         content.startsWith("// exact-check\n");
188     m.exports.should_fail = content.indexOf("\n// should-fail\n") !== -1 ||
189         content.startsWith("// should-fail\n");
190     return m.exports;
191 }
192
193 function readFile(filePath) {
194     return fs.readFileSync(filePath, 'utf8');
195 }
196
197 function loadThings(thingsToLoad, kindOfLoad, funcToCall, fileContent) {
198     var content = '';
199     for (var i = 0; i < thingsToLoad.length; ++i) {
200         var tmp = funcToCall(fileContent, thingsToLoad[i]);
201         if (tmp === null) {
202             console.log('unable to find ' + kindOfLoad + ' "' + thingsToLoad[i] + '"');
203             process.exit(1);
204         }
205         content += tmp;
206         content += 'exports.' + thingsToLoad[i] + ' = ' + thingsToLoad[i] + ';';
207     }
208     return content;
209 }
210
211 function contentToDiffLine(key, value) {
212     return `"${key}": "${value}",`;
213 }
214
215 // This function is only called when no matching result was found and therefore will only display
216 // the diff between the two items.
217 function betterLookingDiff(entry, data) {
218     let output = ' {\n';
219     let spaces = '     ';
220     for (let key in entry) {
221         if (!entry.hasOwnProperty(key)) {
222             continue;
223         }
224         if (!data || !data.hasOwnProperty(key)) {
225             output += '-' + spaces + contentToDiffLine(key, entry[key]) + '\n';
226             continue;
227         }
228         let value = data[key];
229         if (value !== entry[key]) {
230             output += '-' + spaces + contentToDiffLine(key, entry[key]) + '\n';
231             output += '+' + spaces + contentToDiffLine(key, value) + '\n';
232         } else {
233             output += spaces + contentToDiffLine(key, value) + '\n';
234         }
235     }
236     return output + ' }';
237 }
238
239 function lookForEntry(entry, data) {
240     for (var i = 0; i < data.length; ++i) {
241         var allGood = true;
242         for (var key in entry) {
243             if (!entry.hasOwnProperty(key)) {
244                 continue;
245             }
246             var value = data[i][key];
247             // To make our life easier, if there is a "parent" type, we add it to the path.
248             if (key === 'path' && data[i]['parent'] !== undefined) {
249                 if (value.length > 0) {
250                     value += '::' + data[i]['parent']['name'];
251                 } else {
252                     value = data[i]['parent']['name'];
253                 }
254             }
255             if (value !== entry[key]) {
256                 allGood = false;
257                 break;
258             }
259         }
260         if (allGood === true) {
261             return i;
262         }
263     }
264     return null;
265 }
266
267 function loadSearchJsAndIndex(searchJs, searchIndex, storageJs, crate) {
268     if (searchIndex[searchIndex.length - 1].length === 0) {
269         searchIndex.pop();
270     }
271     searchIndex.pop();
272     var fullSearchIndex = searchIndex.join("\n") + '\nexports.rawSearchIndex = searchIndex;';
273     searchIndex = loadContent(fullSearchIndex);
274     var finalJS = "";
275
276     var arraysToLoad = ["itemTypes"];
277     var variablesToLoad = ["MAX_LEV_DISTANCE", "MAX_RESULTS", "NO_TYPE_FILTER",
278                            "GENERICS_DATA", "NAME", "INPUTS_DATA", "OUTPUT_DATA",
279                            "TY_PRIMITIVE", "TY_KEYWORD",
280                            "levenshtein_row2"];
281     // execQuery first parameter is built in getQuery (which takes in the search input).
282     // execQuery last parameter is built in buildIndex.
283     // buildIndex requires the hashmap from search-index.
284     var functionsToLoad = ["buildHrefAndPath", "pathSplitter", "levenshtein", "validateResult",
285                            "buildIndex", "execQuery", "parseQuery", "createQueryResults",
286                            "isWhitespace", "isSpecialStartCharacter", "isStopCharacter",
287                            "parseInput", "getItemsBefore", "getNextElem", "createQueryElement",
288                            "isReturnArrow", "isPathStart", "getStringElem", "newParsedQuery",
289                            "itemTypeFromName", "isEndCharacter", "isErrorCharacter",
290                            "isIdentCharacter", "isSeparatorCharacter", "getIdentEndPosition",
291                            "checkExtraTypeFilterCharacters", "isWhitespaceCharacter"];
292
293     const functions = ["hasOwnPropertyRustdoc", "onEach"];
294     ALIASES = {};
295     finalJS += 'window = { "currentCrate": "' + crate + '", rootPath: "../" };\n';
296     finalJS += loadThings(functions, 'function', extractFunction, storageJs);
297     finalJS += loadThings(arraysToLoad, 'array', extractArrayVariable, searchJs);
298     finalJS += loadThings(variablesToLoad, 'variable', extractVariable, searchJs);
299     finalJS += loadThings(functionsToLoad, 'function', extractFunction, searchJs);
300
301     var loaded = loadContent(finalJS);
302     var index = loaded.buildIndex(searchIndex.rawSearchIndex);
303
304     return [loaded, index];
305 }
306
307 // This function checks if `expected` has all the required fields needed for the checks.
308 function checkNeededFields(fullPath, expected, error_text, queryName, position) {
309     let fieldsToCheck;
310     if (fullPath.length === 0) {
311         fieldsToCheck = [
312             "foundElems",
313             "original",
314             "returned",
315             "typeFilter",
316             "userQuery",
317             "error",
318         ];
319     } else if (fullPath.endsWith("elems") || fullPath.endsWith("generics")) {
320         fieldsToCheck = [
321             "name",
322             "fullPath",
323             "pathWithoutLast",
324             "pathLast",
325             "generics",
326         ];
327     } else {
328         fieldsToCheck = [];
329     }
330     for (var i = 0; i < fieldsToCheck.length; ++i) {
331         const field = fieldsToCheck[i];
332         if (!expected.hasOwnProperty(field)) {
333             let text = `${queryName}==> Mandatory key \`${field}\` is not present`;
334             if (fullPath.length > 0) {
335                 text += ` in field \`${fullPath}\``;
336                 if (position != null) {
337                     text += ` (position ${position})`;
338                 }
339             }
340             error_text.push(text);
341         }
342     }
343 }
344
345 function valueCheck(fullPath, expected, result, error_text, queryName) {
346     if (Array.isArray(expected)) {
347         for (var i = 0; i < expected.length; ++i) {
348             checkNeededFields(fullPath, expected[i], error_text, queryName, i);
349             if (i >= result.length) {
350                 error_text.push(`${queryName}==> EXPECTED has extra value in array from field ` +
351                     `\`${fullPath}\` (position ${i}): \`${JSON.stringify(expected[i])}\``);
352             } else {
353                 valueCheck(fullPath + '[' + i + ']', expected[i], result[i], error_text, queryName);
354             }
355         }
356         for (; i < result.length; ++i) {
357             error_text.push(`${queryName}==> RESULT has extra value in array from field ` +
358                 `\`${fullPath}\` (position ${i}): \`${JSON.stringify(result[i])}\` ` +
359                 'compared to EXPECTED');
360         }
361     } else if (expected !== null && typeof expected !== "undefined" &&
362                expected.constructor == Object)
363     {
364         for (const key in expected) {
365             if (!expected.hasOwnProperty(key)) {
366                 continue;
367             }
368             if (!result.hasOwnProperty(key)) {
369                 error_text.push('==> Unknown key "' + key + '"');
370                 break;
371             }
372             const obj_path = fullPath + (fullPath.length > 0 ? '.' : '') + key;
373             valueCheck(obj_path, expected[key], result[key], error_text, queryName);
374         }
375     } else {
376         expectedValue = JSON.stringify(expected);
377         resultValue = JSON.stringify(result);
378         if (expectedValue != resultValue) {
379             error_text.push(`${queryName}==> Different values for field \`${fullPath}\`:\n` +
380                 `EXPECTED: \`${expectedValue}\`\nRESULT:   \`${resultValue}\``);
381         }
382     }
383 }
384
385 function runParser(query, expected, loaded, loadedFile, queryName) {
386     var error_text = [];
387     checkNeededFields("", expected, error_text, queryName, null);
388     if (error_text.length === 0) {
389         valueCheck('', expected, loaded.parseQuery(query), error_text, queryName);
390     }
391     return error_text;
392 }
393
394 function runSearch(query, expected, index, loaded, loadedFile, queryName) {
395     const filter_crate = loadedFile.FILTER_CRATE;
396     const ignore_order = loadedFile.ignore_order;
397     const exact_check = loadedFile.exact_check;
398
399     var results = loaded.execQuery(loaded.parseQuery(query), index, filter_crate);
400     var error_text = [];
401
402     for (var key in expected) {
403         if (!expected.hasOwnProperty(key)) {
404             continue;
405         }
406         if (!results.hasOwnProperty(key)) {
407             error_text.push('==> Unknown key "' + key + '"');
408             break;
409         }
410         var entry = expected[key];
411
412         if (exact_check == true && entry.length !== results[key].length) {
413             error_text.push(queryName + "==> Expected exactly " + entry.length +
414                             " results but found " + results[key].length + " in '" + key + "'");
415         }
416
417         var prev_pos = -1;
418         for (var i = 0; i < entry.length; ++i) {
419             var entry_pos = lookForEntry(entry[i], results[key]);
420             if (entry_pos === null) {
421                 error_text.push(queryName + "==> Result not found in '" + key + "': '" +
422                                 JSON.stringify(entry[i]) + "'");
423                 // By default, we just compare the two first items.
424                 let item_to_diff = 0;
425                 if ((ignore_order === false || exact_check === true) && i < results[key].length) {
426                     item_to_diff = i;
427                 }
428                 error_text.push("Diff of first error:\n" +
429                     betterLookingDiff(entry[i], results[key][item_to_diff]));
430             } else if (exact_check === true && prev_pos + 1 !== entry_pos) {
431                 error_text.push(queryName + "==> Exact check failed at position " + (prev_pos + 1) +
432                                 ": expected '" + JSON.stringify(entry[i]) + "' but found '" +
433                                 JSON.stringify(results[key][i]) + "'");
434             } else if (ignore_order === false && entry_pos < prev_pos) {
435                 error_text.push(queryName + "==> '" + JSON.stringify(entry[i]) + "' was supposed " +
436                                 "to be before '" + JSON.stringify(results[key][entry_pos]) + "'");
437             } else {
438                 prev_pos = entry_pos;
439             }
440         }
441     }
442     return error_text;
443 }
444
445 function checkResult(error_text, loadedFile, displaySuccess) {
446     if (error_text.length === 0 && loadedFile.should_fail === true) {
447         console.log("FAILED");
448         console.log("==> Test was supposed to fail but all items were found...");
449     } else if (error_text.length !== 0 && loadedFile.should_fail === false) {
450         console.log("FAILED");
451         console.log(error_text.join("\n"));
452     } else {
453         if (displaySuccess) {
454             console.log("OK");
455         }
456         return 0;
457     }
458     return 1;
459 }
460
461 function runCheck(loadedFile, key, callback) {
462     const expected = loadedFile[key];
463     const query = loadedFile.QUERY;
464
465     if (Array.isArray(query)) {
466         if (!Array.isArray(expected)) {
467             console.log("FAILED");
468             console.log(`==> If QUERY variable is an array, ${key} should be an array too`);
469             return 1;
470         } else if (query.length !== expected.length) {
471             console.log("FAILED");
472             console.log(`==> QUERY variable should have the same length as ${key}`);
473             return 1;
474         }
475         for (var i = 0; i < query.length; ++i) {
476             var error_text = callback(query[i], expected[i], "[ query `" + query[i] + "`]");
477             if (checkResult(error_text, loadedFile, false) !== 0) {
478                 return 1;
479             }
480         }
481         console.log("OK");
482     } else {
483         var error_text = callback(query, expected, "");
484         if (checkResult(error_text, loadedFile, true) !== 0) {
485             return 1;
486         }
487     }
488     return 0;
489 }
490
491 function runChecks(testFile, loaded, index) {
492     var checkExpected = false;
493     var checkParsed = false;
494     var testFileContent = readFile(testFile) + 'exports.QUERY = QUERY;';
495
496     if (testFileContent.indexOf("FILTER_CRATE") !== -1) {
497         testFileContent += "exports.FILTER_CRATE = FILTER_CRATE;";
498     } else {
499         testFileContent += "exports.FILTER_CRATE = null;";
500     }
501
502     if (testFileContent.indexOf("\nconst EXPECTED") !== -1) {
503         testFileContent += 'exports.EXPECTED = EXPECTED;';
504         checkExpected = true;
505     }
506     if (testFileContent.indexOf("\nconst PARSED") !== -1) {
507         testFileContent += 'exports.PARSED = PARSED;';
508         checkParsed = true;
509     }
510     if (!checkParsed && !checkExpected) {
511         console.log("FAILED");
512         console.log("==> At least `PARSED` or `EXPECTED` is needed!");
513         return 1;
514     }
515
516     const loadedFile = loadContent(testFileContent);
517     var res = 0;
518
519     if (checkExpected) {
520         res += runCheck(loadedFile, "EXPECTED", (query, expected, text) => {
521             return runSearch(query, expected, index, loaded, loadedFile, text);
522         });
523     }
524     if (checkParsed) {
525         res += runCheck(loadedFile, "PARSED", (query, expected, text) => {
526             return runParser(query, expected, loaded, loadedFile, text);
527         });
528     }
529     return res;
530 }
531
532 function load_files(doc_folder, resource_suffix, crate) {
533     var searchJs = readFile(path.join(doc_folder, "search" + resource_suffix + ".js"));
534     var storageJs = readFile(path.join(doc_folder, "storage" + resource_suffix + ".js"));
535     var searchIndex = readFile(
536         path.join(doc_folder, "search-index" + resource_suffix + ".js")).split("\n");
537
538     return loadSearchJsAndIndex(searchJs, searchIndex, storageJs, crate);
539 }
540
541 function showHelp() {
542     console.log("rustdoc-js options:");
543     console.log("  --doc-folder [PATH]        : location of the generated doc folder");
544     console.log("  --help                     : show this message then quit");
545     console.log("  --crate-name [STRING]      : crate name to be used");
546     console.log("  --test-file [PATHs]        : location of the JS test files (can be called " +
547                 "multiple times)");
548     console.log("  --test-folder [PATH]       : location of the JS tests folder");
549     console.log("  --resource-suffix [STRING] : suffix to refer to the correct files");
550 }
551
552 function parseOptions(args) {
553     var opts = {
554         "crate_name": "",
555         "resource_suffix": "",
556         "doc_folder": "",
557         "test_folder": "",
558         "test_file": [],
559     };
560     var correspondences = {
561         "--resource-suffix": "resource_suffix",
562         "--doc-folder": "doc_folder",
563         "--test-folder": "test_folder",
564         "--test-file": "test_file",
565         "--crate-name": "crate_name",
566     };
567
568     for (var i = 0; i < args.length; ++i) {
569         if (correspondences.hasOwnProperty(args[i])) {
570             i += 1;
571             if (i >= args.length) {
572                 console.log("Missing argument after `" + args[i - 1] + "` option.");
573                 return null;
574             }
575             if (args[i - 1] !== "--test-file") {
576                 opts[correspondences[args[i - 1]]] = args[i];
577             } else {
578                 opts[correspondences[args[i - 1]]].push(args[i]);
579             }
580         } else if (args[i] === "--help") {
581             showHelp();
582             process.exit(0);
583         } else {
584             console.log("Unknown option `" + args[i] + "`.");
585             console.log("Use `--help` to see the list of options");
586             return null;
587         }
588     }
589     if (opts["doc_folder"].length < 1) {
590         console.log("Missing `--doc-folder` option.");
591     } else if (opts["crate_name"].length < 1) {
592         console.log("Missing `--crate-name` option.");
593     } else if (opts["test_folder"].length < 1 && opts["test_file"].length < 1) {
594         console.log("At least one of `--test-folder` or `--test-file` option is required.");
595     } else {
596         return opts;
597     }
598     return null;
599 }
600
601 function checkFile(test_file, opts, loaded, index) {
602     const test_name = path.basename(test_file, ".js");
603
604     process.stdout.write('Checking "' + test_name + '" ... ');
605     return runChecks(test_file, loaded, index);
606 }
607
608 function main(argv) {
609     var opts = parseOptions(argv.slice(2));
610     if (opts === null) {
611         return 1;
612     }
613
614     var [loaded, index] = load_files(
615         opts["doc_folder"],
616         opts["resource_suffix"],
617         opts["crate_name"]);
618     var errors = 0;
619
620     if (opts["test_file"].length !== 0) {
621         opts["test_file"].forEach(function(file) {
622             errors += checkFile(file, opts, loaded, index);
623         });
624     } else if (opts["test_folder"].length !== 0) {
625         fs.readdirSync(opts["test_folder"]).forEach(function(file) {
626             if (!file.endsWith(".js")) {
627                 return;
628             }
629             errors += checkFile(path.join(opts["test_folder"], file), opts, loaded, index);
630         });
631     }
632     return errors > 0 ? 1 : 0;
633 }
634
635 process.exit(main(process.argv));