]> git.lizzy.rs Git - rust.git/blob - src/tools/rustdoc-gui/tester.js
Rollup merge of #106600 - compiler-errors:no-private-field-ty-err, r=estebank
[rust.git] / src / tools / rustdoc-gui / tester.js
1 // This package needs to be install:
2 //
3 // ```
4 // npm install browser-ui-test
5 // ```
6
7 const fs = require("fs");
8 const path = require("path");
9 const os = require('os');
10 const {Options, runTest} = require('browser-ui-test');
11
12 // If a test fails or errors, we will retry it two more times in case it was a flaky failure.
13 const NB_RETRY = 3;
14
15 function showHelp() {
16     console.log("rustdoc-js options:");
17     console.log("  --doc-folder [PATH]        : location of the generated doc folder");
18     console.log("  --file [PATH]              : file to run (can be repeated)");
19     console.log("  --debug                    : show extra information about script run");
20     console.log("  --show-text                : render font in pages");
21     console.log("  --no-headless              : disable headless mode");
22     console.log("  --no-sandbox               : disable sandbox mode");
23     console.log("  --help                     : show this message then quit");
24     console.log("  --tests-folder [PATH]      : location of the .GOML tests folder");
25     console.log("  --jobs [NUMBER]            : number of threads to run tests on");
26     console.log("  --executable-path [PATH]   : path of the browser's executable to be used");
27 }
28
29 function isNumeric(s) {
30     return /^\d+$/.test(s);
31 }
32
33 function parseOptions(args) {
34     var opts = {
35         "doc_folder": "",
36         "tests_folder": "",
37         "files": [],
38         "debug": false,
39         "show_text": false,
40         "no_headless": false,
41         "jobs": -1,
42         "executable_path": null,
43         "no_sandbox": false,
44     };
45     var correspondances = {
46         "--doc-folder": "doc_folder",
47         "--tests-folder": "tests_folder",
48         "--debug": "debug",
49         "--show-text": "show_text",
50         "--no-headless": "no_headless",
51         "--executable-path": "executable_path",
52         "--no-sandbox": "no_sandbox",
53     };
54
55     for (var i = 0; i < args.length; ++i) {
56         if (args[i] === "--doc-folder"
57             || args[i] === "--tests-folder"
58             || args[i] === "--file"
59             || args[i] === "--jobs"
60             || args[i] === "--executable-path") {
61             i += 1;
62             if (i >= args.length) {
63                 console.log("Missing argument after `" + args[i - 1] + "` option.");
64                 return null;
65             }
66             if (args[i - 1] === "--jobs") {
67                 if (!isNumeric(args[i])) {
68                     console.log(
69                         "`--jobs` option expects a positive number, found `" + args[i] + "`");
70                     return null;
71                 }
72                 opts["jobs"] = parseInt(args[i]);
73             } else if (args[i - 1] !== "--file") {
74                 opts[correspondances[args[i - 1]]] = args[i];
75             } else {
76                 opts["files"].push(args[i]);
77             }
78         } else if (args[i] === "--help") {
79             showHelp();
80             process.exit(0);
81         } else if (args[i] === "--no-sandbox") {
82             console.log("`--no-sandbox` is being used. Be very careful!");
83             opts[correspondances[args[i]]] = true;
84         } else if (correspondances[args[i]]) {
85             opts[correspondances[args[i]]] = true;
86         } else {
87             console.log("Unknown option `" + args[i] + "`.");
88             console.log("Use `--help` to see the list of options");
89             return null;
90         }
91     }
92     if (opts["tests_folder"].length < 1) {
93         console.log("Missing `--tests-folder` option.");
94     } else if (opts["doc_folder"].length < 1) {
95         console.log("Missing `--doc-folder` option.");
96     } else {
97         return opts;
98     }
99     return null;
100 }
101
102 /// Print single char status information without \n
103 function char_printer(n_tests) {
104     const max_per_line = 10;
105     let current = 0;
106     return {
107         successful: function() {
108             current += 1;
109             if (current % max_per_line === 0) {
110                 process.stdout.write(`. (${current}/${n_tests})${os.EOL}`);
111             } else {
112                 process.stdout.write(".");
113             }
114         },
115         erroneous: function() {
116             current += 1;
117             if (current % max_per_line === 0) {
118                 process.stderr.write(`F (${current}/${n_tests})${os.EOL}`);
119             } else {
120                 process.stderr.write("F");
121             }
122         },
123         finish: function() {
124             if (current % max_per_line === 0) {
125                 // Don't output if we are already at a matching line end
126                 console.log("");
127             } else {
128                 const spaces = " ".repeat(max_per_line - (current % max_per_line));
129                 process.stdout.write(`${spaces} (${current}/${n_tests})${os.EOL}${os.EOL}`);
130             }
131         },
132     };
133 }
134
135 // Sort array by .file_name property
136 function by_filename(a, b) {
137     return a.file_name - b.file_name;
138 }
139
140 async function runTests(opts, framework_options, files, results, status_bar, showTestFailures) {
141     const tests_queue = [];
142
143     for (const testPath of files) {
144         const callback = runTest(testPath, framework_options)
145             .then(out => {
146                 const [output, nb_failures] = out;
147                 results[nb_failures === 0 ? "successful" : "failed"].push({
148                     file_name: testPath,
149                     output: output,
150                 });
151                 if (nb_failures === 0) {
152                     status_bar.successful();
153                 } else if (showTestFailures) {
154                     status_bar.erroneous();
155                 }
156             })
157             .catch(err => {
158                 results.errored.push({
159                     file_name: testPath,
160                     output: err,
161                 });
162                 if (showTestFailures) {
163                     status_bar.erroneous();
164                 }
165             })
166             .finally(() => {
167                 // We now remove the promise from the tests_queue.
168                 tests_queue.splice(tests_queue.indexOf(callback), 1);
169             });
170         tests_queue.push(callback);
171         if (opts["jobs"] > 0 && tests_queue.length >= opts["jobs"]) {
172             await Promise.race(tests_queue);
173         }
174     }
175     if (tests_queue.length > 0) {
176         await Promise.all(tests_queue);
177     }
178 }
179
180 function createEmptyResults() {
181     return {
182         successful: [],
183         failed: [],
184         errored: [],
185     };
186 }
187
188 async function main(argv) {
189     let opts = parseOptions(argv.slice(2));
190     if (opts === null) {
191         process.exit(1);
192     }
193
194     // Print successful tests too
195     let debug = false;
196     // Run tests in sequentially
197     let headless = true;
198     const framework_options = new Options();
199     try {
200         // This is more convenient that setting fields one by one.
201         let args = [
202             "--variable", "DOC_PATH", opts["doc_folder"], "--enable-fail-on-js-error",
203             "--allow-file-access-from-files",
204         ];
205         if (opts["debug"]) {
206             debug = true;
207             args.push("--debug");
208         }
209         if (opts["show_text"]) {
210             args.push("--show-text");
211         }
212         if (opts["no_sandbox"]) {
213             args.push("--no-sandbox");
214         }
215         if (opts["no_headless"]) {
216             args.push("--no-headless");
217             headless = false;
218         }
219         if (opts["executable_path"] !== null) {
220             args.push("--executable-path");
221             args.push(opts["executable_path"]);
222         }
223         framework_options.parseArguments(args);
224     } catch (error) {
225         console.error(`invalid argument: ${error}`);
226         process.exit(1);
227     }
228
229     let files;
230     if (opts["files"].length === 0) {
231         files = fs.readdirSync(opts["tests_folder"]);
232     } else {
233         files = opts["files"];
234     }
235     files = files.filter(file => path.extname(file) == ".goml");
236     if (files.length === 0) {
237         console.error("rustdoc-gui: No test selected");
238         process.exit(2);
239     }
240     files.forEach((file_name, index) => {
241         files[index] = path.join(opts["tests_folder"], file_name);
242     });
243     files.sort();
244
245     if (!headless) {
246         opts["jobs"] = 1;
247         console.log("`--no-headless` option is active, disabling concurrency for running tests.");
248     }
249
250     console.log(`Running ${files.length} rustdoc-gui (${opts["jobs"]} concurrently) ...`);
251
252     if (opts["jobs"] < 1) {
253         process.setMaxListeners(files.length + 1);
254     } else if (headless) {
255         process.setMaxListeners(opts["jobs"] + 1);
256     }
257
258     // We catch this "event" to display a nicer message in case of unexpected exit (because of a
259     // missing `--no-sandbox`).
260     const exitHandling = (code) => {
261         if (!opts["no_sandbox"]) {
262             console.log("");
263             console.log(
264                 "`browser-ui-test` crashed unexpectedly. Please try again with adding `--test-args \
265 --no-sandbox` at the end. For example: `x.py test src/test/rustdoc-gui --test-args --no-sandbox`");
266             console.log("");
267         }
268     };
269     process.on('exit', exitHandling);
270
271     const originalFilesLen = files.length;
272     let results = createEmptyResults();
273     const status_bar = char_printer(files.length);
274
275     let new_results;
276     for (let it = 0; it < NB_RETRY && files.length > 0; ++it) {
277         new_results = createEmptyResults();
278         await runTests(opts, framework_options, files, new_results, status_bar, it + 1 >= NB_RETRY);
279         Array.prototype.push.apply(results.successful, new_results.successful);
280         // We generate the new list of files with the previously failing tests.
281         files = Array.prototype.concat(new_results.failed, new_results.errored);
282         if (files.length > originalFilesLen / 2) {
283             // If we have too many failing tests, it's very likely not flaky failures anymore so
284             // no need to retry.
285             break;
286         }
287     }
288
289     status_bar.finish();
290
291     Array.prototype.push.apply(results.failed, new_results.failed);
292     Array.prototype.push.apply(results.errored, new_results.errored);
293
294     // We don't need this listener anymore.
295     process.removeListener("exit", exitHandling);
296
297     if (debug) {
298         results.successful.sort(by_filename);
299         results.successful.forEach(r => {
300             console.log(r.output);
301         });
302     }
303
304     if (results.failed.length > 0) {
305         console.log("");
306         results.failed.sort(by_filename);
307         results.failed.forEach(r => {
308             console.log(r.file_name, r.output);
309         });
310     }
311     if (results.errored.length > 0) {
312         console.log(os.EOL);
313         // print run errors on the bottom so developers see them better
314         results.errored.sort(by_filename);
315         results.errored.forEach(r => {
316             console.error(r.file_name, r.output);
317         });
318     }
319
320     if (results.failed.length > 0 || results.errored.length > 0) {
321         process.exit(1);
322     }
323 }
324
325 main(process.argv);