| // This package needs to be install: |
| // |
| // ``` |
| // npm install browser-ui-test |
| // ``` |
| |
| const fs = require("fs"); |
| const path = require("path"); |
| const os = require("os"); |
| const {Options, runTest} = require("browser-ui-test"); |
| |
| // If a test fails or errors, we will retry it two more times in case it was a flaky failure. |
| const NB_RETRY = 3; |
| |
| function showHelp() { |
| console.log("rustdoc-js options:"); |
| console.log(" --doc-folder [PATH] : location of the generated doc folder"); |
| console.log(" --file [PATH] : file to run (can be repeated)"); |
| console.log(" --debug : show extra information about script run"); |
| console.log(" --show-text : render font in pages"); |
| console.log(" --no-headless : disable headless mode"); |
| console.log(" --help : show this message then quit"); |
| console.log(" --tests-folder [PATH] : location of the .GOML tests folder"); |
| console.log(" --jobs [NUMBER] : number of threads to run tests on"); |
| console.log(" --executable-path [PATH] : path of the browser's executable to be used"); |
| } |
| |
| function isNumeric(s) { |
| return /^\d+$/.test(s); |
| } |
| |
| function parseOptions(args) { |
| const opts = { |
| "doc_folder": "", |
| "tests_folder": "", |
| "files": [], |
| "debug": false, |
| "show_text": false, |
| "no_headless": false, |
| "jobs": -1, |
| "executable_path": null, |
| }; |
| const correspondences = { |
| "--doc-folder": "doc_folder", |
| "--tests-folder": "tests_folder", |
| "--debug": "debug", |
| "--show-text": "show_text", |
| "--no-headless": "no_headless", |
| "--executable-path": "executable_path", |
| }; |
| |
| for (let i = 0; i < args.length; ++i) { |
| const arg = args[i]; |
| if (arg === "--doc-folder" |
| || arg === "--tests-folder" |
| || arg === "--file" |
| || arg === "--jobs" |
| || arg === "--executable-path") { |
| i += 1; |
| if (i >= args.length) { |
| console.log("Missing argument after `" + arg + "` option."); |
| return null; |
| } |
| const arg_value = args[i]; |
| if (arg === "--jobs") { |
| if (!isNumeric(arg_value)) { |
| console.log( |
| "`--jobs` option expects a positive number, found `" + arg_value + "`"); |
| return null; |
| } |
| opts["jobs"] = parseInt(arg_value); |
| } else if (arg !== "--file") { |
| opts[correspondences[arg]] = arg_value; |
| } else { |
| opts["files"].push(arg_value); |
| } |
| } else if (arg === "--help") { |
| showHelp(); |
| process.exit(0); |
| } else if (correspondences[arg]) { |
| opts[correspondences[arg]] = true; |
| } else { |
| console.log("Unknown option `" + arg + "`."); |
| console.log("Use `--help` to see the list of options"); |
| return null; |
| } |
| } |
| if (opts["tests_folder"].length < 1) { |
| console.log("Missing `--tests-folder` option."); |
| } else if (opts["doc_folder"].length < 1) { |
| console.log("Missing `--doc-folder` option."); |
| } else { |
| return opts; |
| } |
| return null; |
| } |
| |
| /// Print single char status information without \n |
| function char_printer(n_tests) { |
| const max_per_line = 10; |
| let current = 0; |
| return { |
| successful: function() { |
| current += 1; |
| if (current % max_per_line === 0) { |
| process.stdout.write(`. (${current}/${n_tests})${os.EOL}`); |
| } else { |
| process.stdout.write("."); |
| } |
| }, |
| erroneous: function() { |
| current += 1; |
| if (current % max_per_line === 0) { |
| process.stderr.write(`F (${current}/${n_tests})${os.EOL}`); |
| } else { |
| process.stderr.write("F"); |
| } |
| }, |
| finish: function() { |
| if (current % max_per_line === 0) { |
| // Don't output if we are already at a matching line end |
| console.log(""); |
| } else { |
| const spaces = " ".repeat(max_per_line - (current % max_per_line)); |
| process.stdout.write(`${spaces} (${current}/${n_tests})${os.EOL}${os.EOL}`); |
| } |
| }, |
| }; |
| } |
| |
| // Sort array by .file_name property |
| function by_filename(a, b) { |
| return a.file_name - b.file_name; |
| } |
| |
| async function runTests(opts, framework_options, files, results, status_bar, showTestFailures) { |
| const tests_queue = []; |
| |
| for (const testPath of files) { |
| const callback = runTest(testPath, {"options": framework_options}) |
| .then(out => { |
| const [output, nb_failures] = out; |
| results[nb_failures === 0 ? "successful" : "failed"].push({ |
| file_name: testPath, |
| output: output, |
| }); |
| if (nb_failures === 0) { |
| status_bar.successful(); |
| } else if (showTestFailures) { |
| status_bar.erroneous(); |
| } |
| }) |
| .catch(err => { |
| results.errored.push({ |
| file_name: testPath, |
| output: err, |
| }); |
| if (showTestFailures) { |
| status_bar.erroneous(); |
| } |
| }) |
| .finally(() => { |
| // We now remove the promise from the tests_queue. |
| tests_queue.splice(tests_queue.indexOf(callback), 1); |
| }); |
| tests_queue.push(callback); |
| if (opts["jobs"] > 0 && tests_queue.length >= opts["jobs"]) { |
| await Promise.race(tests_queue); |
| } |
| } |
| if (tests_queue.length > 0) { |
| await Promise.all(tests_queue); |
| } |
| } |
| |
| function createEmptyResults() { |
| return { |
| successful: [], |
| failed: [], |
| errored: [], |
| }; |
| } |
| |
| async function main(argv) { |
| const opts = parseOptions(argv.slice(2)); |
| if (opts === null) { |
| process.exit(1); |
| } |
| |
| // Print successful tests too |
| let debug = false; |
| // Run tests in sequentially |
| let headless = true; |
| const framework_options = new Options(); |
| try { |
| // This is more convenient that setting fields one by one. |
| const args = [ |
| "--variable", "DOC_PATH", opts["doc_folder"].split("\\").join("/"), |
| "--enable-fail-on-js-error", "--allow-file-access-from-files", |
| "--no-sandbox", |
| ]; |
| if (opts["debug"]) { |
| debug = true; |
| args.push("--debug"); |
| } |
| if (opts["show_text"]) { |
| args.push("--show-text"); |
| } |
| if (opts["no_headless"]) { |
| args.push("--no-headless"); |
| headless = false; |
| } |
| if (opts["executable_path"] !== null) { |
| args.push("--executable-path"); |
| args.push(opts["executable_path"]); |
| } |
| framework_options.parseArguments(args); |
| } catch (error) { |
| console.error(`invalid argument: ${error}`); |
| process.exit(1); |
| } |
| |
| let files; |
| if (opts["files"].length === 0) { |
| files = fs.readdirSync(opts["tests_folder"]); |
| } else { |
| files = opts["files"]; |
| } |
| files = files.filter(file => path.extname(file) === ".goml"); |
| if (files.length === 0) { |
| console.error("rustdoc-gui: No test selected"); |
| process.exit(2); |
| } |
| files.forEach((file_name, index) => { |
| files[index] = path.join(opts["tests_folder"], file_name); |
| }); |
| files.sort(); |
| |
| if (!headless) { |
| opts["jobs"] = 1; |
| console.log("`--no-headless` option is active, disabling concurrency for running tests."); |
| } |
| |
| if (opts["jobs"] < 1) { |
| const len = files.length; |
| console.log( |
| `Running ${len} rustdoc-gui (UNBOUNDED concurrency; use "-j#" for a limit) ...`, |
| ); |
| process.setMaxListeners(files.length + 1); |
| } else if (headless) { |
| console.log(`Running ${files.length} rustdoc-gui (${opts["jobs"]} concurrently) ...`); |
| process.setMaxListeners(opts["jobs"] + 1); |
| } else { |
| console.log(`Running ${files.length} rustdoc-gui ...`); |
| } |
| |
| const originalFilesLen = files.length; |
| const results = createEmptyResults(); |
| const status_bar = char_printer(files.length); |
| |
| let new_results; |
| for (let it = 0; it < NB_RETRY && files.length > 0; ++it) { |
| new_results = createEmptyResults(); |
| await runTests(opts, framework_options, files, new_results, status_bar, it + 1 >= NB_RETRY); |
| Array.prototype.push.apply(results.successful, new_results.successful); |
| // We generate the new list of files with the previously failing tests. |
| files = Array.prototype.concat(new_results.failed, new_results.errored).map( |
| f => f["file_name"]); |
| if (files.length > originalFilesLen / 2) { |
| // If we have too many failing tests, it's very likely not flaky failures anymore so |
| // no need to retry. |
| break; |
| } |
| } |
| |
| status_bar.finish(); |
| |
| Array.prototype.push.apply(results.failed, new_results.failed); |
| Array.prototype.push.apply(results.errored, new_results.errored); |
| |
| if (debug) { |
| results.successful.sort(by_filename); |
| results.successful.forEach(r => { |
| console.log(r.output); |
| }); |
| } |
| |
| if (results.failed.length > 0) { |
| console.log(""); |
| results.failed.sort(by_filename); |
| results.failed.forEach(r => { |
| console.log(r.file_name, r.output); |
| }); |
| } |
| if (results.errored.length > 0) { |
| console.log(os.EOL); |
| // print run errors on the bottom so developers see them better |
| results.errored.sort(by_filename); |
| results.errored.forEach(r => { |
| console.error(r.file_name, r.output); |
| }); |
| } |
| |
| if (results.failed.length > 0 || results.errored.length > 0) { |
| process.exit(1); |
| } |
| process.exit(0); |
| } |
| |
| main(process.argv); |