From ae789dd22d001b231c5602dd4da8f3f632cd8efe Mon Sep 17 00:00:00 2001 From: joshsiv-mulga Date: Mon, 18 May 2026 17:32:38 +1000 Subject: [PATCH] test(e2e): add harness.Phase / Step / Detail for human-readable test output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test fixtures were emitting flat walls of t.Logf with every line prefixed by the same `lb_test.go:61:` (file:line of the call site that the t.Helper() chain bubbles up to) and forced 6-space indent. Hard to skim, hard to find where one phase ends and the next begins. Add three helpers in tests/e2e/harness/log.go: Phase(t, fmt, ...) — flush-left colored banner via fmt.Println. Bypasses t.Logf so the line lands without the testing framework's indent/file:line prefix. Pure visual structure; the diagnostic detail stays on t.Logf. Step(t, fmt, ...) — single-line sub-phase marker via t.Logf. Keeps file:line attribution so a triager can find the source of a progress message. Detail(t, k, v, ...) — key=value resource IDs via t.Logf. Same attribution rationale as Step — IDs are diagnostic and want to land in JUnit XML bucketed with the owning test. Why the split: Phase is purely a banner; losing JUnit attribution costs nothing because banners aren't diagnostic. Step/Detail carry triage context (instance IDs, AMI, etc.) and a failing test's JUnit testcase node should still contain them, so t.Logf attribution is kept. ANSI colors are auto-disabled when stdout isn't a TTY and GITHUB_ACTIONS isn't set, so piped local `go test -v` stays clean. Apply to lb_test setupSharedFixture — five Phase banners replace the implicit run of helper logs ("Discovering cluster capacity", "Ensuring SSH key pair", "Creating shared VPC topology", "Launching app instances", "Launching probe client") so the live CI log reads as a story rather than a flat resource dump. --- tests/e2e/harness/log.go | 66 ++++++++++++++++++++++++++++++++++++++++ tests/e2e/lb/lb_test.go | 11 +++++-- 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/harness/log.go diff --git a/tests/e2e/harness/log.go b/tests/e2e/harness/log.go new file mode 100644 index 000000000..8d998c9a1 --- /dev/null +++ b/tests/e2e/harness/log.go @@ -0,0 +1,66 @@ +//go:build e2e + +package harness + +import ( + "fmt" + "os" + "strings" + "testing" +) + +// ANSI color codes — rendered live by the Actions UI; raw escapes pass +// through to artifact log files harmlessly. Disabled when stdout is not +// a TTY and GITHUB_ACTIONS isn't set, so piped local `go test -v` stays +// clean. +var ( + colorBold = "\x1b[1m" + colorDim = "\x1b[2m" + colorCyan = "\x1b[36m" + colorReset = "\x1b[0m" +) + +func init() { + if os.Getenv("GITHUB_ACTIONS") == "" && os.Getenv("FORCE_COLOR") == "" { + if fi, _ := os.Stdout.Stat(); fi == nil || (fi.Mode()&os.ModeCharDevice) == 0 { + colorBold, colorDim, colorCyan, colorReset = "", "", "", "" + } + } +} + +// Phase writes a colored section banner directly to stdout, bypassing +// t.Logf so the line lands flush-left without the testing framework's +// `::` prefix or forced indent. Banners are pure visual +// structure — the diagnostic resource IDs themselves stay on t.Logf +// (see Detail) so they remain attributed to the owning test in JUnit XML. +// +// Use sparingly: one line per logical phase, not per attribute. +func Phase(t *testing.T, format string, args ...any) { + t.Helper() + fmt.Fprintf(os.Stdout, "\n%s%s━━ %s ━━%s\n", colorBold, colorCyan, fmt.Sprintf(format, args...), colorReset) +} + +// Step emits a single-line sub-phase marker via t.Logf so it stays +// attributed to the owning test in JUnit XML. Indent + file:line prefix +// are accepted as the cost of correct attribution — these lines carry +// progress detail (e.g. "waiting for instance running") that a triager +// will want bucketed with the failing testcase. +func Step(t *testing.T, format string, args ...any) { + t.Helper() + t.Logf("%s· %s%s", colorDim, fmt.Sprintf(format, args...), colorReset) +} + +// Detail emits a key=value line via t.Logf — same attribution rationale +// as Step. Pass alternating key, value, key, value …; mismatched lengths +// panic so the bug surfaces at the test boundary. +func Detail(t *testing.T, kvs ...any) { + t.Helper() + if len(kvs)%2 != 0 { + panic("harness.Detail: odd number of arguments; want key, value pairs") + } + var parts []string + for i := 0; i < len(kvs); i += 2 { + parts = append(parts, fmt.Sprintf("%s%v%s=%v", colorDim, kvs[i], colorReset, kvs[i+1])) + } + t.Logf("%s", strings.Join(parts, " ")) +} diff --git a/tests/e2e/lb/lb_test.go b/tests/e2e/lb/lb_test.go index 02a030d30..656ff05b9 100644 --- a/tests/e2e/lb/lb_test.go +++ b/tests/e2e/lb/lb_test.go @@ -116,24 +116,29 @@ type sharedFixture struct { func setupSharedFixture(t *testing.T, c *harness.AWSClient, artifacts string) *sharedFixture { t.Helper() f := &sharedFixture{} + + harness.Phase(t, "Discovering cluster capacity") f.InstanceType = discoverNanoInstanceType(t, c) f.AMIID = discoverAMI(t, c) + + harness.Phase(t, "Ensuring SSH key pair %q", lbKeyName) ensureKeyPair(t, c) t.Cleanup(func() { deleteKeyPair(t, c) }) + harness.Phase(t, "Creating shared VPC topology (%s)", lbVPCCIDR) createVPC(t, c, f) t.Cleanup(func() { deleteVPC(t, c, f) }) - createIGW(t, c, f) t.Cleanup(func() { deleteIGW(t, c, f) }) - createSubnet(t, c, f) t.Cleanup(func() { deleteSubnet(t, c, f) }) - configureDefaultSG(t, c, f) + + harness.Phase(t, "Launching app instances (2× %s)", f.InstanceType) launchAppInstances(t, c, f) t.Cleanup(func() { terminateInstances(t, c, f.AppInstanceIDs) }) + harness.Phase(t, "Launching probe client") launchSharedProbeClient(t, c, f) t.Cleanup(func() { terminateInstances(t, c, []string{f.ClientID}) })