1#!/usr/bin/env node --redirect-warnings=/dev/null
2
3const fs = require("fs");
4const path = require("path");
5const { spawnSync } = require("child_process");
6
7const FAILING_SEED_REGEX = /failing seed: (\d+)/gi;
8const CARGO_TEST_ARGS = ["--release", "--lib", "--package", "collab"];
9
10if (require.main === module) {
11 if (process.argv.length < 4) {
12 process.stderr.write(
13 "usage: script/randomized-test-minimize <input-plan> <output-plan> [start-index]\n",
14 );
15 process.exit(1);
16 }
17
18 minimizeTestPlan(
19 process.argv[2],
20 process.argv[3],
21 parseInt(process.argv[4]) || 0,
22 );
23}
24
25function minimizeTestPlan(inputPlanPath, outputPlanPath, startIndex = 0) {
26 const tempPlanPath = inputPlanPath + ".try";
27
28 fs.copyFileSync(inputPlanPath, outputPlanPath);
29 let testPlan = JSON.parse(fs.readFileSync(outputPlanPath, "utf8"));
30
31 process.stderr.write("minimizing failing test plan...\n");
32 for (let ix = startIndex; ix < testPlan.length; ix++) {
33 // Skip 'MutateClients' entries, since they themselves are not single operations.
34 if (testPlan[ix].MutateClients) {
35 continue;
36 }
37
38 // Remove a row from the test plan
39 const newTestPlan = testPlan.slice();
40 newTestPlan.splice(ix, 1);
41 fs.writeFileSync(tempPlanPath, serializeTestPlan(newTestPlan), "utf8");
42
43 process.stderr.write(
44 `${ix}/${testPlan.length}: ${JSON.stringify(testPlan[ix])}`,
45 );
46 const failingSeed = runTests({
47 SEED: "0",
48 LOAD_PLAN: tempPlanPath,
49 SAVE_PLAN: tempPlanPath,
50 ITERATIONS: "500",
51 });
52
53 // If the test failed, keep the test plan with the removed row. Reload the test
54 // plan from the JSON file, since the test itself will remove any operations
55 // which are no longer valid before saving the test plan.
56 if (failingSeed != null) {
57 process.stderr.write(` - remove. failing seed: ${failingSeed}.\n`);
58 fs.copyFileSync(tempPlanPath, outputPlanPath);
59 testPlan = JSON.parse(fs.readFileSync(outputPlanPath, "utf8"));
60 ix--;
61 } else {
62 process.stderr.write(` - keep.\n`);
63 }
64 }
65
66 fs.unlinkSync(tempPlanPath);
67
68 // Re-run the final minimized plan to get the correct failing seed.
69 // This is a workaround for the fact that the execution order can
70 // slightly change when replaying a test plan after it has been
71 // saved and loaded.
72 const failingSeed = runTests({
73 SEED: "0",
74 ITERATIONS: "5000",
75 LOAD_PLAN: outputPlanPath,
76 });
77
78 process.stderr.write(`final test plan: ${outputPlanPath}\n`);
79 process.stderr.write(`final seed: ${failingSeed}\n`);
80 return failingSeed;
81}
82
83function buildTests() {
84 const { status } = spawnSync(
85 "cargo",
86 ["test", "--no-run", ...CARGO_TEST_ARGS],
87 {
88 stdio: "inherit",
89 encoding: "utf8",
90 env: {
91 ...process.env,
92 },
93 },
94 );
95 if (status !== 0) {
96 throw new Error("build failed");
97 }
98}
99
100function runTests(env) {
101 const { status, stdout } = spawnSync(
102 "cargo",
103 ["test", ...CARGO_TEST_ARGS, "random_project_collaboration"],
104 {
105 stdio: "pipe",
106 encoding: "utf8",
107 env: {
108 ...process.env,
109 ...env,
110 },
111 },
112 );
113
114 if (status !== 0) {
115 FAILING_SEED_REGEX.lastIndex = 0;
116 const match = FAILING_SEED_REGEX.exec(stdout);
117 if (!match) {
118 process.stderr.write("test failed, but no failing seed found:\n");
119 process.stderr.write(stdout);
120 process.stderr.write("\n");
121 process.exit(1);
122 }
123 return match[1];
124 } else {
125 return null;
126 }
127}
128
129function serializeTestPlan(plan) {
130 return "[\n" + plan.map((row) => JSON.stringify(row)).join(",\n") + "\n]\n";
131}
132
133exports.buildTests = buildTests;
134exports.runTests = runTests;
135exports.minimizeTestPlan = minimizeTestPlan;