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