Files
sanderling/cmd/internal-tools/analyze/censoring_test.go
T
pj 1008402558 fix(analyze): score a seed pair by which run outlived the other
The paired path had the same defect as the unpaired one: it subtracted two
step counts and handed the differences to the signed-rank test, so a pair
holding a run the wall clock stopped at step 12 entered as a difference
neither run supports. Twenty seeds where the first arm was still clean at
step 12 and the second violated at step 5 in six of them read sign -1 and
p 0.0011, pointing at the arm that never violated.

A pair is now scored the way the unpaired comparison scores one and tested by
the exact sign test over the pairs whose order censoring determines, which is
what the log-rank stratified by seed reduces to here. The signed-rank goes
with the differences it needed: a magnitude-based paired test wants a
difference from every pair, and the arms censor on different clocks. The
median difference stays, over the pairs where both runs violated, and says so.
2026-08-18 20:16:40 +05:30

119 lines
4.3 KiB
Go

package main
import (
"math"
"path/filepath"
"testing"
)
// A run stops at whichever comes first, the step budget or the campaign's wall
// clock, so an arm that spends more wall clock per step leaves runs censored far
// below the budget. Those runs are not observations of a violation at that step,
// and the comparison between arms has to read them as the bounds they are.
type wallClockRun struct {
steps int
violated bool
}
func stoppedShort(count, steps int) []wallClockRun {
runs := make([]wallClockRun, 0, count)
for index := 0; index < count; index++ {
runs = append(runs, wallClockRun{steps: steps})
}
return runs
}
func violatedAt(count, steps int) []wallClockRun {
runs := make([]wallClockRun, 0, count)
for index := 0; index < count; index++ {
runs = append(runs, wallClockRun{steps: steps, violated: true})
}
return runs
}
func writeWallClockCampaign(t *testing.T, directory, name string, budget int, runs []wallClockRun) string {
t.Helper()
seeds := make([]int, 0, len(runs))
records := make([]map[string]any, 0, len(runs))
for index, run := range runs {
seed := index + 1
seeds = append(seeds, seed)
record := map[string]any{
"seed": seed, "exit_code": 0, "steps": run.steps, "actions": run.steps,
"monotonic_millis": 60_000,
}
if run.violated {
record["first_violation_origin_step"] = run.steps
record["violated_properties"] = []string{"plantedProperty"}
}
records = append(records, record)
}
writeCampaign(t, directory, map[string]any{
"arm": name, "generator": "seeded", "platform": "android",
"max_steps": budget, "seeds": seeds,
}, records)
return directory
}
func wallClockArms(t *testing.T, early, late []wallClockRun) (string, string) {
t.Helper()
root := t.TempDir()
return writeWallClockCampaign(t, filepath.Join(root, "early"), "early", 400, early),
writeWallClockCampaign(t, filepath.Join(root, "late"), "late", 400, late)
}
// Twenty runs stopped clean at step 12 against twenty violations at step 100.
// Nothing in the first arm was observed past step 12, so no pair of runs across
// the arms has a determined order and there is no difference to report.
func TestRun_RunsStoppedBeforeEveryEventCarryNoComparison(t *testing.T) {
earlyDirectory, lateDirectory := wallClockArms(t, stoppedShort(20, 12), violatedAt(20, 100))
pair := analyseCampaigns(t, earlyDirectory, lateDirectory).Pairwise[0]
if pair.First != "early" || pair.Second != "late" {
t.Fatalf("comparison %s vs %s, want early vs late", pair.First, pair.Second)
}
if math.Abs(pair.A12-0.5) > 1e-12 {
t.Errorf("a12 %.4f between an arm censored at 12 and one violating at 100, want 0.5: "+
"a run that stopped at step 12 never reached step 100", pair.A12)
}
if pair.PValue < 0.05 {
t.Errorf("p %.3e, want no significant difference: the arms were never observed over the same steps",
pair.PValue)
}
}
// Where the two arms were observed together, over the first twelve steps, the
// arm the wall clock stopped is the one that did not violate. The effect size
// has to follow that and not the step counts the flattening reads.
func TestRun_EffectSizeFollowsWhatCensoringDetermines(t *testing.T) {
late := append(violatedAt(6, 5), violatedAt(14, 100)...)
earlyDirectory, lateDirectory := wallClockArms(t, stoppedShort(20, 12), late)
pair := analyseCampaigns(t, earlyDirectory, lateDirectory).Pairwise[0]
if pair.A12 <= 0.5 {
t.Errorf("a12 %.4f, want above 0.5: six of the late arm's runs violated by step 5 "+
"and none of the early arm's twenty had violated by step 12", pair.A12)
}
}
// The seed-matched contrast reads the same censored runs and reaches the same
// conclusion or it is not measuring the same thing.
func TestRun_PairedContrastFollowsWhatCensoringDetermines(t *testing.T) {
late := append(violatedAt(6, 5), violatedAt(14, 100)...)
earlyDirectory, lateDirectory := wallClockArms(t, stoppedShort(20, 12), late)
result := analyseCampaigns(t, "--paired", earlyDirectory, lateDirectory)
paired := *result.Paired
if paired.First != "early" || paired.Second != "late" {
t.Fatalf("paired %s minus %s, want early minus late", paired.First, paired.Second)
}
if paired.Sign != 1 {
t.Errorf("sign %+d, want +1: the late arm is the one seen to violate first, in the six pairs "+
"where the order is determined at all", paired.Sign)
}
if paired.A12 <= 0.5 {
t.Errorf("a12 within pairs %.4f, want above 0.5", paired.A12)
}
}