|
5 | 5 | // Usage: |
6 | 6 | // APM_PYTHON_BIN=/path/to/apm go test -count=1 -json ./... | go run .crane/scripts/score.go |
7 | 7 | // |
8 | | -// This script implements the deletion-grade framework from issue #96. |
| 8 | +// This script implements the deletion-grade framework from issues #78 and #96. |
9 | 9 | // migration_score = 1.0 only when ALL of the following gates pass: |
10 | 10 | // |
11 | 11 | // Gate 1 -- python_reference_required: APM_PYTHON_BIN must be set and valid. |
|
15 | 15 | // Gate 2 -- go_tests_pass: every Go test in the module must pass. A single |
16 | 16 | // failing non-parity test voids the gate. |
17 | 17 | // |
18 | | -// Gate 3 -- help_parity: TestParityCompletionCommandMatrix must pass. |
19 | | -// Every public command must respond to --help with exit 0. |
| 18 | +// Gate 3 -- surface_parity: TestParityCompletionSurfaceParity must pass. |
| 19 | +// Python and Go command/option/subcommand inventories must match. |
20 | 20 | // |
21 | | -// Gate 4 -- version_parity: TestParityCompletionVersionEquivalent must pass. |
| 21 | +// Gate 4 -- help_parity: TestParityCompletionCommandMatrix and |
| 22 | +// TestParityCompletionHelpIdentical must pass. Every public help |
| 23 | +// and invalid-usage path must match Python. |
22 | 24 | // |
23 | | -// Gate 5 -- init_parity: TestParityCompletionInitParity must pass. |
24 | | -// The init command must produce apm.yml equivalent to Python. |
| 25 | +// Gate 5 -- functional_contracts: TestParityCompletionFunctionalContracts |
| 26 | +// must pass. Supported command behavior must be covered by |
| 27 | +// black-box Python-vs-Go contracts. |
25 | 28 | // |
26 | | -// Gate 6 -- error_parity: TestParityCompletionErrorParity must pass. |
27 | | -// Unknown commands must produce matching non-zero exit codes. |
| 29 | +// Gate 6 -- state_diff_contracts: TestParityCompletionStateDiffContracts |
| 30 | +// must pass. Mutating commands must match Python filesystem, |
| 31 | +// lockfile, config, cache, and generated-artifact effects. |
28 | 32 | // |
29 | | -// Gate 7 -- no_known_exceptions: the test output must not contain any |
| 33 | +// Gate 7 -- python_tests_pass: TestParityCompletionPythonSuite must pass. |
| 34 | +// The original Python reference test suite must still be green. |
| 35 | +// |
| 36 | +// Gate 8 -- benchmarks_pass: TestParityCompletionBenchmarks must pass. |
| 37 | +// Migration benchmarks must run and satisfy the configured guard. |
| 38 | +// |
| 39 | +// Gate 9 -- no_known_exceptions: the test output must not contain any |
30 | 40 | // "approved exception" log line. Final cutover requires zero exceptions. |
31 | 41 | // |
32 | 42 | // If Gate 1 fails, migration_score is forced to 0.0 regardless of other gates. |
@@ -73,13 +83,16 @@ func main() { |
73 | 83 | scanner := bufio.NewScanner(os.Stdin) |
74 | 84 | scanner.Buffer(make([]byte, 4*1024*1024), 4*1024*1024) |
75 | 85 |
|
76 | | - // Deletion-grade gate test names (exact or prefix). |
| 86 | + // Deletion-grade gate test names. |
77 | 87 | const ( |
78 | | - gateHardGate = "TestParityCompletionHardGate" |
79 | | - gateCmdMatrix = "TestParityCompletionCommandMatrix" |
80 | | - gateVersionParity = "TestParityCompletionVersionEquivalent" |
81 | | - gateInitParity = "TestParityCompletionInitParity" |
82 | | - gateErrorParity = "TestParityCompletionErrorParity" |
| 88 | + gateHardGate = "TestParityCompletionHardGate" |
| 89 | + gateSurfaceParity = "TestParityCompletionSurfaceParity" |
| 90 | + gateCommandMatrix = "TestParityCompletionCommandMatrix" |
| 91 | + gateHelpIdentical = "TestParityCompletionHelpIdentical" |
| 92 | + gateFunctionalContracts = "TestParityCompletionFunctionalContracts" |
| 93 | + gateStateDiffContracts = "TestParityCompletionStateDiffContracts" |
| 94 | + gatePythonSuite = "TestParityCompletionPythonSuite" |
| 95 | + gateBenchmarks = "TestParityCompletionBenchmarks" |
83 | 96 | ) |
84 | 97 |
|
85 | 98 | // Track per-test pass/fail. |
@@ -151,64 +164,39 @@ func main() { |
151 | 164 | gate2.Reason = fmt.Sprintf("%d/%d tests passing", passingTests, totalTests) |
152 | 165 | } |
153 | 166 |
|
154 | | - // Gate 3: help_parity (command matrix) |
155 | | - gate3 := GateResult{Name: "help_parity"} |
156 | | - if testPassed[gateCmdMatrix] && !testFailed[gateCmdMatrix] { |
157 | | - gate3.Passing = true |
158 | | - } else if testFailed[gateCmdMatrix] { |
159 | | - gate3.Passing = false |
160 | | - gate3.Reason = "TestParityCompletionCommandMatrix failed" |
161 | | - } else { |
162 | | - gate3.Passing = false |
163 | | - gate3.Reason = "TestParityCompletionCommandMatrix not found" |
164 | | - } |
| 167 | + // Gate 3: surface_parity |
| 168 | + gate3 := singleTestGate("surface_parity", gateSurfaceParity, testPassed, testFailed) |
165 | 169 |
|
166 | | - // Gate 4: version_parity |
167 | | - gate4 := GateResult{Name: "version_parity"} |
168 | | - if testPassed[gateVersionParity] && !testFailed[gateVersionParity] { |
169 | | - gate4.Passing = true |
170 | | - } else if testFailed[gateVersionParity] { |
171 | | - gate4.Passing = false |
172 | | - gate4.Reason = "TestParityCompletionVersionEquivalent failed" |
173 | | - } else { |
174 | | - gate4.Passing = false |
175 | | - gate4.Reason = "TestParityCompletionVersionEquivalent not found" |
176 | | - } |
| 170 | + // Gate 4: help_parity |
| 171 | + gate4 := multiTestGate( |
| 172 | + "help_parity", |
| 173 | + []string{gateCommandMatrix, gateHelpIdentical}, |
| 174 | + testPassed, |
| 175 | + testFailed, |
| 176 | + ) |
177 | 177 |
|
178 | | - // Gate 5: init_parity |
179 | | - gate5 := GateResult{Name: "init_parity"} |
180 | | - if testPassed[gateInitParity] && !testFailed[gateInitParity] { |
181 | | - gate5.Passing = true |
182 | | - } else if testFailed[gateInitParity] { |
183 | | - gate5.Passing = false |
184 | | - gate5.Reason = "TestParityCompletionInitParity failed" |
185 | | - } else { |
186 | | - gate5.Passing = false |
187 | | - gate5.Reason = "TestParityCompletionInitParity not found" |
188 | | - } |
| 178 | + // Gate 5: functional_contracts |
| 179 | + gate5 := singleTestGate("functional_contracts", gateFunctionalContracts, testPassed, testFailed) |
189 | 180 |
|
190 | | - // Gate 6: error_parity |
191 | | - gate6 := GateResult{Name: "error_parity"} |
192 | | - if testPassed[gateErrorParity] && !testFailed[gateErrorParity] { |
193 | | - gate6.Passing = true |
194 | | - } else if testFailed[gateErrorParity] { |
195 | | - gate6.Passing = false |
196 | | - gate6.Reason = "TestParityCompletionErrorParity failed" |
197 | | - } else { |
198 | | - gate6.Passing = false |
199 | | - gate6.Reason = "TestParityCompletionErrorParity not found" |
200 | | - } |
| 181 | + // Gate 6: state_diff_contracts |
| 182 | + gate6 := singleTestGate("state_diff_contracts", gateStateDiffContracts, testPassed, testFailed) |
| 183 | + |
| 184 | + // Gate 7: python_tests_pass |
| 185 | + gate7 := singleTestGate("python_tests_pass", gatePythonSuite, testPassed, testFailed) |
| 186 | + |
| 187 | + // Gate 8: benchmarks_pass |
| 188 | + gate8 := singleTestGate("benchmarks_pass", gateBenchmarks, testPassed, testFailed) |
201 | 189 |
|
202 | | - // Gate 7: no_known_exceptions |
203 | | - gate7 := GateResult{Name: "no_known_exceptions"} |
| 190 | + // Gate 9: no_known_exceptions |
| 191 | + gate9 := GateResult{Name: "no_known_exceptions"} |
204 | 192 | if knownExceptionsFound { |
205 | | - gate7.Passing = false |
206 | | - gate7.Reason = "output contains 'approved exception' -- all exceptions must be resolved for cutover" |
| 193 | + gate9.Passing = false |
| 194 | + gate9.Reason = "output contains 'approved exception' -- all exceptions must be resolved for cutover" |
207 | 195 | } else { |
208 | | - gate7.Passing = true |
| 196 | + gate9.Passing = true |
209 | 197 | } |
210 | 198 |
|
211 | | - gates := []GateResult{gate1, gate2, gate3, gate4, gate5, gate6, gate7} |
| 199 | + gates := []GateResult{gate1, gate2, gate3, gate4, gate5, gate6, gate7, gate8, gate9} |
212 | 200 |
|
213 | 201 | // Count parity tests (any test with "Parity" in name from cmd/apm). |
214 | 202 | parityPassing, parityTotal := 0, 0 |
@@ -257,3 +245,35 @@ func main() { |
257 | 245 | out, _ := json.MarshalIndent(score, "", " ") |
258 | 246 | fmt.Println(string(out)) |
259 | 247 | } |
| 248 | + |
| 249 | +func singleTestGate(name, testName string, testPassed, testFailed map[string]bool) GateResult { |
| 250 | + return multiTestGate(name, []string{testName}, testPassed, testFailed) |
| 251 | +} |
| 252 | + |
| 253 | +func multiTestGate(name string, testNames []string, testPassed, testFailed map[string]bool) GateResult { |
| 254 | + for _, testName := range testNames { |
| 255 | + if testFailed[testName] { |
| 256 | + return GateResult{ |
| 257 | + Name: name, |
| 258 | + Passing: false, |
| 259 | + Reason: testName + " failed", |
| 260 | + } |
| 261 | + } |
| 262 | + } |
| 263 | + |
| 264 | + var missing []string |
| 265 | + for _, testName := range testNames { |
| 266 | + if !testPassed[testName] { |
| 267 | + missing = append(missing, testName) |
| 268 | + } |
| 269 | + } |
| 270 | + if len(missing) > 0 { |
| 271 | + return GateResult{ |
| 272 | + Name: name, |
| 273 | + Passing: false, |
| 274 | + Reason: strings.Join(missing, ", ") + " not found", |
| 275 | + } |
| 276 | + } |
| 277 | + |
| 278 | + return GateResult{Name: name, Passing: true} |
| 279 | +} |
0 commit comments