Files
sanderling/cmd/internal-tools/analyze/incomplete_test.go
T
pj b99da0be0e feat(analyze): time an event at the step it was detected and report the quartiles
an obligation that never discharges is reported when the run ends, and timing it by the step that armed it recorded a liveness failure flushed at the budget as a violation found on step 1. the survival analysis now measures the detected step, falling back to the origin for campaigns written before the field existed, and says how many events that moved. the report gains the first and third quartiles beside the median.
2026-08-16 17:45:20 +05:30

156 lines
6.6 KiB
Go

package main
import (
"bytes"
"io"
"path/filepath"
"strings"
"testing"
)
// A campaign that produced nothing still has a manifest, and the manifest is
// what makes the difference between an arm that ran nothing and an arm that was
// never scheduled. Reading one must report every intended seed as missing rather
// than an arm with a small clean sample.
func TestRun_EmptyCampaignIsReportedAsEverySeedMissing(t *testing.T) {
root := t.TempDir()
empty := filepath.Join(root, "empty")
writeCampaign(t, empty, map[string]any{
"arm": "seeded", "max_steps": 400, "seeds": []int{1, 2, 3, 4, 5},
}, nil)
var stdout bytes.Buffer
if err := run([]string{empty}, &stdout, io.Discard); err != nil {
t.Fatal(err)
}
result := analyseCampaigns(t, empty)
summary := armByName(t, result, "seeded")
if summary.Recorded != 0 || summary.Usable != 0 {
t.Errorf("recorded %d usable %d, want none", summary.Recorded, summary.Usable)
}
if len(summary.MissingSeeds) != 5 {
t.Errorf("missing seeds %v, want all five", summary.MissingSeeds)
}
if summary.MedianStepsToFirstViolation != nil || summary.ViolationRate != nil {
t.Errorf("median %v violation rate %v, want neither from no runs",
summary.MedianStepsToFirstViolation, summary.ViolationRate)
}
if result.LogRank != nil || len(result.Pairwise) != 0 {
t.Errorf("log-rank %+v pairwise %v, want no tests", result.LogRank, result.Pairwise)
}
if !strings.Contains(stdout.String(), "seeded") {
t.Errorf("the empty arm is not in the report\n%s", stdout.String())
}
}
// A host that stopped part way through leaves a directory that looks complete.
// The seeds it never reached are the difference between a partial campaign and
// a smaller one, and the report has to carry that count.
func TestRun_PartialCampaignCountsTheSeedsTheHostNeverReached(t *testing.T) {
root := t.TempDir()
partial := filepath.Join(root, "partial")
var records []map[string]any
for seed := 1; seed <= 6; seed++ {
records = append(records, map[string]any{
"seed": seed, "exit_code": 0, "steps": 400, "actions": 380, "monotonic_millis": 360000,
})
}
writeCampaign(t, partial, map[string]any{
"arm": "seeded", "max_steps": 400,
"seeds": []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
}, records)
summary := armByName(t, analyseCampaigns(t, partial), "seeded")
if summary.Usable != 6 {
t.Errorf("%d usable runs, want the 6 that landed", summary.Usable)
}
if len(summary.MissingSeeds) != 4 {
t.Errorf("missing seeds %v, want the 4 the host never reached", summary.MissingSeeds)
}
var stdout bytes.Buffer
if err := run([]string{partial}, &stdout, io.Discard); err != nil {
t.Fatal(err)
}
if !strings.Contains(stdout.String(), "missing") {
t.Errorf("no missing-seed column in the report\n%s", stdout.String())
}
}
// The ablation is seed-matched, so a run lost on one arm removes its partner
// from the comparison too. Those seeds are named because a paired sample that
// silently shrinks is how a campaign reports a difference between two arms that
// were not in fact matched.
func TestRun_PairedComparisonNamesSeedsLostOnOneArm(t *testing.T) {
root := t.TempDir()
declared := []int{1, 2, 3, 4, 5, 6}
pre := filepath.Join(root, "pre")
post := filepath.Join(root, "post")
writeCampaign(t, pre, map[string]any{"arm": "pre", "max_steps": 400, "seeds": declared}, []map[string]any{
{"seed": 1, "exit_code": 0, "steps": 300, "actions": 290, "monotonic_millis": 1000, "first_violation_origin_step": 300, "violated_properties": []string{"p"}},
{"seed": 2, "exit_code": 0, "steps": 400, "actions": 390, "monotonic_millis": 1000},
{"seed": 3, "exit_code": 0, "steps": 250, "actions": 240, "monotonic_millis": 1000, "first_violation_origin_step": 250, "violated_properties": []string{"p"}},
{"seed": 4, "exit_code": -1, "timed_out": true, "actions": 0},
})
writeCampaign(t, post, map[string]any{"arm": "post", "max_steps": 400, "seeds": declared}, []map[string]any{
{"seed": 1, "exit_code": 0, "steps": 40, "actions": 38, "monotonic_millis": 1000, "first_violation_origin_step": 40, "violated_properties": []string{"p"}},
{"seed": 2, "exit_code": 0, "steps": 60, "actions": 55, "monotonic_millis": 1000, "first_violation_origin_step": 60, "violated_properties": []string{"p"}},
{"seed": 3, "exit_code": 0, "steps": 50, "actions": 47, "monotonic_millis": 1000, "first_violation_origin_step": 50, "violated_properties": []string{"p"}},
{"seed": 4, "exit_code": 0, "steps": 70, "actions": 66, "monotonic_millis": 1000, "first_violation_origin_step": 70, "violated_properties": []string{"p"}},
})
result := analyseCampaigns(t, "--paired", pre, post)
if result.Paired == nil {
t.Fatal("no paired comparison")
}
if result.Paired.Pairs != 3 {
t.Errorf("%d pairs, want the 3 seeds usable on both arms", result.Paired.Pairs)
}
if len(result.Paired.UnpairedSeeds) != 1 || result.Paired.UnpairedSeeds[0] != 4 {
t.Errorf("unpaired seeds %v, want [4]", result.Paired.UnpairedSeeds)
}
for _, summary := range result.Arms {
if len(summary.MissingSeeds) != 2 {
t.Errorf("arm %s missing seeds %v, want seeds 5 and 6", summary.Arm, summary.MissingSeeds)
}
}
var stdout bytes.Buffer
if err := run([]string{"--paired", pre, post}, &stdout, io.Discard); err != nil {
t.Fatal(err)
}
if !strings.Contains(stdout.String(), "usable in one arm only") {
t.Errorf("the report does not name the lost seed\n%s", stdout.String())
}
}
func TestRun_PairedRefusesAnythingOtherThanTwoArms(t *testing.T) {
root := t.TempDir()
var directories []string
for _, name := range []string{"a", "b", "c"} {
directory := filepath.Join(root, name)
writeCampaign(t, directory, map[string]any{"arm": name, "max_steps": 40, "seeds": []int{1}},
[]map[string]any{{"seed": 1, "exit_code": 0, "steps": 40, "actions": 40}})
directories = append(directories, directory)
}
err := run(append([]string{"--paired"}, directories...), io.Discard, io.Discard)
if err == nil || !strings.Contains(err.Error(), "exactly two arms") {
t.Fatalf("error %v, want a refusal to pair three arms", err)
}
}
func TestRun_PairedRefusesArmsThatShareNoSeed(t *testing.T) {
root := t.TempDir()
north := filepath.Join(root, "north")
south := filepath.Join(root, "south")
writeCampaign(t, north, map[string]any{"arm": "north", "max_steps": 40, "seeds": []int{1, 2}},
[]map[string]any{{"seed": 1, "exit_code": 0, "steps": 40, "actions": 40}})
writeCampaign(t, south, map[string]any{"arm": "south", "max_steps": 40, "seeds": []int{3, 4}},
[]map[string]any{{"seed": 3, "exit_code": 0, "steps": 40, "actions": 40}})
err := run([]string{"--paired", north, south}, io.Discard, io.Discard)
if err == nil || !strings.Contains(err.Error(), "share no seed") {
t.Fatalf("error %v, want a refusal to pair arms that ran different seeds", err)
}
}