11//go:build ignore
22
3- // score.go -- deletion-grade migration scoring script for the APM CLI Python-to-Go migration.
3+ // score.go -- deletion-grade migration scoring for the APM CLI Python-to-Go migration.
44// Usage: go test -json ./... | go run .crane/scripts/score.go
55// Outputs JSON that separates progress metrics from cutover-readiness gates.
66package main
@@ -76,6 +76,12 @@ type ProgressMetrics struct {
7676 PerfRatio float64 `json:"perf_ratio"`
7777}
7878
79+ type GateResult struct {
80+ Name string `json:"name"`
81+ Passing bool `json:"passing"`
82+ Reason string `json:"reason,omitempty"`
83+ }
84+
7985type Score struct {
8086 MigrationScore float64 `json:"migration_score"`
8187 Progress float64 `json:"progress"`
@@ -97,6 +103,7 @@ type Score struct {
97103 SourceTestsPassing int `json:"source_tests_passing"`
98104 TargetTestsPassing int `json:"target_tests_passing"`
99105 PerfRatio float64 `json:"perf_ratio"`
106+ Gates []GateResult `json:"gates"`
100107}
101108
102109func main () {
@@ -118,15 +125,15 @@ type scanInput interface {
118125
119126func computeScore (input scanInput , getenv getenvFunc ) (Score , error ) {
120127 scanner := bufio .NewScanner (input )
128+ scanner .Buffer (make ([]byte , 4 * 1024 * 1024 ), 4 * 1024 * 1024 )
121129
122130 var parityPassing , parityTotal , targetPassing , targetTotal int
123131 eventsSeen := 0
124- terminalPackages := 0
125132 goTestsFailed := false
126133 running := map [string ]bool {}
127134 passed := map [string ]bool {}
135+ failed := map [string ]bool {}
128136 knownExceptions := knownExceptionsFromEnv (getenv ("APM_KNOWN_EXCEPTIONS" ))
129- knownExceptionsSeen := getenv ("APM_KNOWN_EXCEPTIONS" ) != ""
130137 pythonReference := BoolGate {}
131138 pythonTests := BoolGate {Seen : getenv ("APM_PYTHON_TESTS" ) != "" , Passed : getenv ("APM_PYTHON_TESTS" ) == "pass" }
132139 benchmarks := BoolGate {Seen : getenv ("APM_BENCHMARKS" ) != "" , Passed : getenv ("APM_BENCHMARKS" ) == "pass" }
@@ -155,7 +162,6 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
155162 case "state_diff" :
156163 stateDiff = RatioGate {Seen : true , Passing : gate .Passing , Total : gate .Total }
157164 case "known_exceptions" :
158- knownExceptionsSeen = true
159165 knownExceptions = gate .Count
160166 case "python_tests" :
161167 pythonTests = BoolGate {Seen : true , Passed : gate .Passed }
@@ -178,9 +184,6 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
178184 }
179185
180186 if ev .Test == "" {
181- if isTargetPackage (ev .Package ) && (ev .Action == "pass" || ev .Action == "fail" ) {
182- terminalPackages ++
183- }
184187 if isTargetPackage (ev .Package ) && ev .Action == "fail" {
185188 goTestsFailed = true
186189 }
@@ -194,11 +197,14 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
194197 case "pass" :
195198 passed [ev .Test ] = true
196199 delete (running , key )
197- case "fail" , "skip" :
200+ case "fail" :
201+ failed [ev .Test ] = true
198202 delete (running , key )
199- if ev . Action == "fail" && isTargetPackage (ev .Package ) {
203+ if isTargetPackage (ev .Package ) {
200204 goTestsFailed = true
201205 }
206+ case "skip" :
207+ delete (running , key )
202208 }
203209
204210 isParity := strings .Contains (ev .Test , "Parity" ) || strings .Contains (ev .Package , "parity" )
@@ -223,27 +229,30 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
223229 if eventsSeen == 0 || targetTotal == 0 {
224230 return Score {}, fmt .Errorf ("Go test event stream is empty or incomplete" )
225231 }
226- if terminalPackages == 0 && ! allExplicitGatesSeen (pythonReference , pythonTests , benchmarks , surface , help , functional , stateDiff , knownExceptionsSeen ) {
227- return Score {}, fmt .Errorf ("Go test event stream is incomplete: no package completion event" )
228- }
229232 if len (running ) > 0 {
230233 return Score {}, fmt .Errorf ("Go test event stream is incomplete: %d test(s) did not finish" , len (running ))
231234 }
232235
233236 if ! pythonReference .Seen {
234- pythonReference = BoolGate {Seen : true , Passed : pythonReferenceReady (getenv ("APM_PYTHON_BIN" ))}
237+ pythonReference = BoolGate {Seen : true , Passed : testPassed ( passed , failed , "TestParityCompletionHardGate" ) || pythonReferenceReady (getenv ("APM_PYTHON_BIN" ))}
235238 }
236239 if ! surface .Seen {
237- surface = RatioGate { Seen : true , Passing : boolToInt ( requiredGatePassed ( passed , "TestParitySurfaceInventory " , "TestParityCompletionCommandMatrix" )), Total : 1 }
240+ surface = inferredAnyRatioGate ( passed , failed , "TestParityCompletionSurfaceParity " , "TestParitySurfaceInventory" )
238241 }
239242 if ! help .Seen {
240- help = RatioGate { Seen : true , Passing : boolToInt ( requiredGatePassed ( passed , "TestParityCompletionHelpIdentical" )), Total : 1 }
243+ help = inferredAllRatioGate ( passed , failed , "TestParityCompletionCommandMatrix" , "TestParityCompletionHelpIdentical" )
241244 }
242245 if ! functional .Seen {
243- functional = RatioGate { Seen : true , Passing : boolToInt ( requiredGatePassed ( passed , "TestParityFunctionalContracts" )), Total : 1 }
246+ functional = inferredAnyRatioGate ( passed , failed , "TestParityCompletionFunctionalContracts" , "TestParityFunctionalContracts" )
244247 }
245248 if ! stateDiff .Seen {
246- stateDiff = RatioGate {Seen : true , Passing : boolToInt (requiredGatePassed (passed , "TestParityStateDiffContracts" )), Total : 1 }
249+ stateDiff = inferredAnyRatioGate (passed , failed , "TestParityCompletionStateDiffContracts" , "TestParityStateDiffContracts" )
250+ }
251+ if ! pythonTests .Seen {
252+ pythonTests = BoolGate {Seen : true , Passed : testPassed (passed , failed , "TestParityCompletionPythonSuite" )}
253+ }
254+ if ! benchmarks .Seen {
255+ benchmarks = BoolGate {Seen : true , Passed : testPassed (passed , failed , "TestParityCompletionBenchmarks" )}
247256 }
248257
249258 goTestsPass := ! goTestsFailed && targetTotal > 0 && targetPassing == targetTotal
@@ -274,7 +283,6 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
274283 gates .HelpParity == 1.0 &&
275284 gates .FunctionalContracts == 1.0 &&
276285 gates .StateDiffContracts == 1.0 &&
277- knownExceptionsSeen &&
278286 gates .KnownExceptions == 0 &&
279287 gates .GoTests == "pass" &&
280288 gates .PythonTests == "pass" &&
@@ -287,7 +295,7 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
287295 if ! cutoverReady && migrationScore >= 1.0 {
288296 migrationScore = 0.999
289297 }
290- if cutoverReady {
298+ if cutoverReady && progress == 1.0 {
291299 migrationScore = 1.0
292300 }
293301
@@ -320,6 +328,7 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
320328 SourceTestsPassing : metrics .SourceTestsPassing ,
321329 TargetTestsPassing : metrics .TargetTestsPassing ,
322330 PerfRatio : metrics .PerfRatio ,
331+ Gates : gateResults (gates ),
323332 }, nil
324333}
325334
@@ -338,7 +347,12 @@ func pythonReferenceReady(bin string) bool {
338347 return info .Mode ()& 0o111 != 0
339348}
340349
341- func requiredGatePassed (passed map [string ]bool , names ... string ) bool {
350+ func testPassed (passed , failed map [string ]bool , names ... string ) bool {
351+ for _ , name := range names {
352+ if failed [name ] {
353+ return false
354+ }
355+ }
342356 for _ , name := range names {
343357 if passed [name ] {
344358 return true
@@ -347,6 +361,47 @@ func requiredGatePassed(passed map[string]bool, names ...string) bool {
347361 return false
348362}
349363
364+ func inferredAnyRatioGate (passed , failed map [string ]bool , names ... string ) RatioGate {
365+ for _ , name := range names {
366+ if failed [name ] {
367+ return RatioGate {Seen : true , Passing : 0 , Total : 1 }
368+ }
369+ }
370+ return RatioGate {Seen : true , Passing : boolToInt (testPassed (passed , failed , names ... )), Total : 1 }
371+ }
372+
373+ func inferredAllRatioGate (passed , failed map [string ]bool , names ... string ) RatioGate {
374+ for _ , name := range names {
375+ if failed [name ] {
376+ return RatioGate {Seen : true , Passing : 0 , Total : 1 }
377+ }
378+ }
379+ return RatioGate {Seen : true , Passing : boolToInt (allRequiredTestsPassed (passed , names ... )), Total : 1 }
380+ }
381+
382+ func allRequiredTestsPassed (passed map [string ]bool , names ... string ) bool {
383+ for _ , name := range names {
384+ if ! passed [name ] {
385+ return false
386+ }
387+ }
388+ return true
389+ }
390+
391+ func gateResults (gates CutoverGates ) []GateResult {
392+ return []GateResult {
393+ {Name : "python_reference_required" , Passing : gates .PythonReferenceRequired },
394+ {Name : "go_tests_pass" , Passing : gates .GoTests == "pass" },
395+ {Name : "surface_parity" , Passing : gates .SurfaceParity == 1.0 },
396+ {Name : "help_parity" , Passing : gates .HelpParity == 1.0 },
397+ {Name : "functional_contracts" , Passing : gates .FunctionalContracts == 1.0 },
398+ {Name : "state_diff_contracts" , Passing : gates .StateDiffContracts == 1.0 },
399+ {Name : "python_tests_pass" , Passing : gates .PythonTests == "pass" },
400+ {Name : "benchmarks_pass" , Passing : gates .Benchmarks == "pass" },
401+ {Name : "no_known_exceptions" , Passing : gates .KnownExceptions == 0 },
402+ }
403+ }
404+
350405func passFail (ok bool ) string {
351406 if ok {
352407 return "pass"
@@ -361,21 +416,6 @@ func boolToInt(ok bool) int {
361416 return 0
362417}
363418
364- func allExplicitGatesSeen (
365- pythonReference BoolGate ,
366- pythonTests BoolGate ,
367- benchmarks BoolGate ,
368- surface RatioGate ,
369- help RatioGate ,
370- functional RatioGate ,
371- stateDiff RatioGate ,
372- knownExceptionsSeen bool ,
373- ) bool {
374- return pythonReference .Seen && pythonTests .Seen && benchmarks .Seen &&
375- surface .Seen && help .Seen && functional .Seen && stateDiff .Seen &&
376- knownExceptionsSeen
377- }
378-
379419func knownExceptionsFromEnv (raw string ) int {
380420 if raw == "" {
381421 return 0
@@ -388,7 +428,7 @@ func knownExceptionsFromEnv(raw string) int {
388428}
389429
390430func approvedExceptionCount (output string ) (int , bool ) {
391- if ! strings .Contains (output , "approved" ) || ! strings .Contains (output , "exception" ) {
431+ if ! strings .Contains (strings . ToLower ( output ) , "approved" ) || ! strings .Contains (strings . ToLower ( output ) , "exception" ) {
392432 return 0 , false
393433 }
394434 fields := strings .Fields (output )
0 commit comments