test(e2e): add harness.Phase / Step / Detail for human-readable test output

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.
This commit is contained in:
joshsiv-mulga
2026-05-18 17:32:38 +10:00
parent be966b20c4
commit ae789dd22d
2 changed files with 74 additions and 3 deletions
+66
View File
@@ -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
// `<file>:<line>:` 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, " "))
}
+8 -3
View File
@@ -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}) })