1 // This package needs to be install:
4 // npm install browser-ui-test
7 const fs = require("fs");
8 const path = require("path");
9 const os = require('os');
10 const {Options, runTest} = require('browser-ui-test');
12 // If a test fails or errors, we will retry it two more times in case it was a flaky failure.
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");
29 function isNumeric(s) {
30 return /^\d+$/.test(s);
33 function parseOptions(args) {
42 "executable_path": null,
45 var correspondances = {
46 "--doc-folder": "doc_folder",
47 "--tests-folder": "tests_folder",
49 "--show-text": "show_text",
50 "--no-headless": "no_headless",
51 "--executable-path": "executable_path",
52 "--no-sandbox": "no_sandbox",
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") {
62 if (i >= args.length) {
63 console.log("Missing argument after `" + args[i - 1] + "` option.");
66 if (args[i - 1] === "--jobs") {
67 if (!isNumeric(args[i])) {
69 "`--jobs` option expects a positive number, found `" + args[i] + "`");
72 opts["jobs"] = parseInt(args[i]);
73 } else if (args[i - 1] !== "--file") {
74 opts[correspondances[args[i - 1]]] = args[i];
76 opts["files"].push(args[i]);
78 } else if (args[i] === "--help") {
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;
87 console.log("Unknown option `" + args[i] + "`.");
88 console.log("Use `--help` to see the list of options");
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.");
102 /// Print single char status information without \n
103 function char_printer(n_tests) {
104 const max_per_line = 10;
107 successful: function() {
109 if (current % max_per_line === 0) {
110 process.stdout.write(`. (${current}/${n_tests})${os.EOL}`);
112 process.stdout.write(".");
115 erroneous: function() {
117 if (current % max_per_line === 0) {
118 process.stderr.write(`F (${current}/${n_tests})${os.EOL}`);
120 process.stderr.write("F");
124 if (current % max_per_line === 0) {
125 // Don't output if we are already at a matching line end
128 const spaces = " ".repeat(max_per_line - (current % max_per_line));
129 process.stdout.write(`${spaces} (${current}/${n_tests})${os.EOL}${os.EOL}`);
135 // Sort array by .file_name property
136 function by_filename(a, b) {
137 return a.file_name - b.file_name;
140 async function runTests(opts, framework_options, files, results, status_bar, showTestFailures) {
141 const tests_queue = [];
143 for (const testPath of files) {
144 const callback = runTest(testPath, framework_options)
146 const [output, nb_failures] = out;
147 results[nb_failures === 0 ? "successful" : "failed"].push({
151 if (nb_failures === 0) {
152 status_bar.successful();
153 } else if (showTestFailures) {
154 status_bar.erroneous();
158 results.errored.push({
162 if (showTestFailures) {
163 status_bar.erroneous();
167 // We now remove the promise from the tests_queue.
168 tests_queue.splice(tests_queue.indexOf(callback), 1);
170 tests_queue.push(callback);
171 if (opts["jobs"] > 0 && tests_queue.length >= opts["jobs"]) {
172 await Promise.race(tests_queue);
175 if (tests_queue.length > 0) {
176 await Promise.all(tests_queue);
180 function createEmptyResults() {
188 async function main(argv) {
189 let opts = parseOptions(argv.slice(2));
194 // Print successful tests too
196 // Run tests in sequentially
198 const framework_options = new Options();
200 // This is more convenient that setting fields one by one.
202 "--variable", "DOC_PATH", opts["doc_folder"], "--enable-fail-on-js-error",
203 "--allow-file-access-from-files",
207 args.push("--debug");
209 if (opts["show_text"]) {
210 args.push("--show-text");
212 if (opts["no_sandbox"]) {
213 args.push("--no-sandbox");
215 if (opts["no_headless"]) {
216 args.push("--no-headless");
219 if (opts["executable_path"] !== null) {
220 args.push("--executable-path");
221 args.push(opts["executable_path"]);
223 framework_options.parseArguments(args);
225 console.error(`invalid argument: ${error}`);
230 if (opts["files"].length === 0) {
231 files = fs.readdirSync(opts["tests_folder"]);
233 files = opts["files"];
235 files = files.filter(file => path.extname(file) == ".goml");
236 if (files.length === 0) {
237 console.error("rustdoc-gui: No test selected");
240 files.forEach((file_name, index) => {
241 files[index] = path.join(opts["tests_folder"], file_name);
247 console.log("`--no-headless` option is active, disabling concurrency for running tests.");
250 console.log(`Running ${files.length} rustdoc-gui (${opts["jobs"]} concurrently) ...`);
252 if (opts["jobs"] < 1) {
253 process.setMaxListeners(files.length + 1);
254 } else if (headless) {
255 process.setMaxListeners(opts["jobs"] + 1);
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"]) {
264 "`browser-ui-test` crashed unexpectedly. Please try again with adding `--test-args \
265 --no-sandbox` at the end. For example: `x.py test tests/rustdoc-gui --test-args --no-sandbox`");
269 process.on('exit', exitHandling);
271 const originalFilesLen = files.length;
272 let results = createEmptyResults();
273 const status_bar = char_printer(files.length);
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).map(
282 f => f['file_name']);
283 if (files.length > originalFilesLen / 2) {
284 // If we have too many failing tests, it's very likely not flaky failures anymore so
292 Array.prototype.push.apply(results.failed, new_results.failed);
293 Array.prototype.push.apply(results.errored, new_results.errored);
295 // We don't need this listener anymore.
296 process.removeListener("exit", exitHandling);
299 results.successful.sort(by_filename);
300 results.successful.forEach(r => {
301 console.log(r.output);
305 if (results.failed.length > 0) {
307 results.failed.sort(by_filename);
308 results.failed.forEach(r => {
309 console.log(r.file_name, r.output);
312 if (results.errored.length > 0) {
314 // print run errors on the bottom so developers see them better
315 results.errored.sort(by_filename);
316 results.errored.forEach(r => {
317 console.error(r.file_name, r.output);
321 if (results.failed.length > 0 || results.errored.length > 0) {