Skip to content

Commit f902200

Browse files
Add retry mechanism for rustdoc GUI tests to reduce flakyness
1 parent c156773 commit f902200

File tree

1 file changed

+76
-46
lines changed

1 file changed

+76
-46
lines changed

src/tools/rustdoc-gui/tester.js

+76-46
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const path = require("path");
99
const os = require('os');
1010
const {Options, runTest} = require('browser-ui-test');
1111

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+
1215
function showHelp() {
1316
console.log("rustdoc-js options:");
1417
console.log(" --doc-folder [PATH] : location of the generated doc folder");
@@ -129,11 +132,59 @@ function char_printer(n_tests) {
129132
};
130133
}
131134

132-
/// Sort array by .file_name property
135+
// Sort array by .file_name property
133136
function by_filename(a, b) {
134137
return a.file_name - b.file_name;
135138
}
136139

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+
137188
async function main(argv) {
138189
let opts = parseOptions(argv.slice(2));
139190
if (opts === null) {
@@ -144,7 +195,7 @@ async function main(argv) {
144195
let debug = false;
145196
// Run tests in sequentially
146197
let headless = true;
147-
const options = new Options();
198+
const framework_options = new Options();
148199
try {
149200
// This is more convenient that setting fields one by one.
150201
let args = [
@@ -169,13 +220,12 @@ async function main(argv) {
169220
args.push("--executable-path");
170221
args.push(opts["executable_path"]);
171222
}
172-
options.parseArguments(args);
223+
framework_options.parseArguments(args);
173224
} catch (error) {
174225
console.error(`invalid argument: ${error}`);
175226
process.exit(1);
176227
}
177228

178-
let failed = false;
179229
let files;
180230
if (opts["files"].length === 0) {
181231
files = fs.readdirSync(opts["tests_folder"]);
@@ -187,6 +237,9 @@ async function main(argv) {
187237
console.error("rustdoc-gui: No test selected");
188238
process.exit(2);
189239
}
240+
files.forEach((file_name, index) => {
241+
files[index] = path.join(opts["tests_folder"], file_name);
242+
});
190243
files.sort();
191244

192245
if (!headless) {
@@ -215,52 +268,29 @@ async function main(argv) {
215268
};
216269
process.on('exit', exitHandling);
217270

218-
const tests_queue = [];
219-
let results = {
220-
successful: [],
221-
failed: [],
222-
errored: [],
223-
};
271+
const originalFilesLen = files.length;
272+
let results = createEmptyResults();
224273
const status_bar = char_printer(files.length);
225-
for (let i = 0; i < files.length; ++i) {
226-
const file_name = files[i];
227-
const testPath = path.join(opts["tests_folder"], file_name);
228-
const callback = runTest(testPath, options)
229-
.then(out => {
230-
const [output, nb_failures] = out;
231-
results[nb_failures === 0 ? "successful" : "failed"].push({
232-
file_name: testPath,
233-
output: output,
234-
});
235-
if (nb_failures > 0) {
236-
status_bar.erroneous();
237-
failed = true;
238-
} else {
239-
status_bar.successful();
240-
}
241-
})
242-
.catch(err => {
243-
results.errored.push({
244-
file_name: testPath + file_name,
245-
output: err,
246-
});
247-
status_bar.erroneous();
248-
failed = true;
249-
})
250-
.finally(() => {
251-
// We now remove the promise from the tests_queue.
252-
tests_queue.splice(tests_queue.indexOf(callback), 1);
253-
});
254-
tests_queue.push(callback);
255-
if (opts["jobs"] > 0 && tests_queue.length >= opts["jobs"]) {
256-
await Promise.race(tests_queue);
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;
257286
}
258287
}
259-
if (tests_queue.length > 0) {
260-
await Promise.all(tests_queue);
261-
}
288+
262289
status_bar.finish();
263290

291+
Array.prototype.push.apply(results.failed, new_results.failed);
292+
Array.prototype.push.apply(results.errored, new_results.errored);
293+
264294
// We don't need this listener anymore.
265295
process.removeListener("exit", exitHandling);
266296

@@ -287,7 +317,7 @@ async function main(argv) {
287317
});
288318
}
289319

290-
if (failed) {
320+
if (results.failed.length > 0 || results.errored.length > 0) {
291321
process.exit(1);
292322
}
293323
}

0 commit comments

Comments
 (0)