From 9c57f7afb339b4811ad5edffeb55a93e6675cbd0 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 18:22:41 +0100 Subject: [PATCH] =?UTF-8?q?mesh-build-go=20publishes=20the=20build=20runne?= =?UTF-8?q?r=20as=20its=20own=20package,=20so=20the=20other=20build=20modu?= =?UTF-8?q?les=20share=20it=20without=20mesh-sdk=20(ADR=200593=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runner, the toolchain guard, the fetch, the parsers, buildcpu, the conformance kit and mesh-pack move from mesh-sdk's buildstep into modules/mesh-build-go/buildstep (own go.mod). mesh-build-go, mesh-build-node and mesh-build-image require it by a same-repository replace; what the delivery parts read (the failure contract, the fetch request, the stale sweep, the module tree shape, NewID, CapacitySetting) stays imported from mesh-sdk as sdkstep. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- modules/mesh-build-go/buildstep/askrun.go | 473 +++++++++++ .../mesh-build-go/buildstep/askrun_test.go | 326 +++++++ modules/mesh-build-go/buildstep/blob.go | 170 ++++ modules/mesh-build-go/buildstep/blob_test.go | 151 ++++ .../buildstep/buildcpu/buildcpu.go | 215 +++++ .../buildstep/buildcpu/buildcpu_test.go | 90 ++ modules/mesh-build-go/buildstep/buildhours.go | 129 +++ modules/mesh-build-go/buildstep/capacity.go | 57 ++ .../mesh-build-go/buildstep/capacity_cpu.go | 141 ++++ .../mesh-build-go/buildstep/capacity_test.go | 135 +++ .../buildstep/cmd/mesh-pack/main.go | 111 +++ .../buildstep/cmd/mesh-pack/main_test.go | 52 ++ .../buildstep/conformance/captured/README.md | 73 ++ .../conformance/captured/go/buildfail.jsonl | 6 + .../conformance/captured/go/deeptimeout.jsonl | 417 +++++++++ .../conformance/captured/go/exiting.jsonl | 10 + .../conformance/captured/go/killed.jsonl | 7 + .../captured/go/logthenfatal.jsonl | 13 + .../conformance/captured/go/mainexit.jsonl | 8 + .../conformance/captured/go/panicking.jsonl | 27 + .../conformance/captured/go/passfirst.jsonl | 22 + .../captured/go/passthenfail.jsonl | 15 + .../conformance/captured/go/race.jsonl | 53 ++ .../conformance/captured/go/racemain.jsonl | 27 + .../conformance/captured/go/subpanic.jsonl | 30 + .../conformance/captured/go/subtests.jsonl | 17 + .../conformance/captured/go/timeout.jsonl | 43 + .../conformance/captured/node/crash.console | 2 + .../conformance/captured/node/crash.tap | 41 + .../captured/node/logcrash.console | 2 + .../conformance/captured/node/logcrash.tap | 42 + .../conformance/captured/node/suite.console | 2 + .../conformance/captured/node/suite.tap | 61 ++ .../conformance/captured/node/syntax.console | 2 + .../conformance/captured/node/syntax.tap | 47 ++ .../conformance/captured/node/timeout.console | 2 + .../conformance/captured/node/timeout.tap | 26 + .../captured/node/two-files.console | 2 + .../conformance/captured/node/two-files.tap | 49 ++ .../captured/tsc/typeerror.console | 1 + .../buildstep/conformance/conformance.go | 201 +++++ .../buildstep/conformance/conformance_test.go | 168 ++++ .../buildstep/conformance/leftrunning.go | 212 +++++ modules/mesh-build-go/buildstep/cpu.go | 14 + .../buildstep/digestshape_test.go | 234 +++++ modules/mesh-build-go/buildstep/fetch.go | 196 +++++ .../buildstep/fetch_integration_test.go | 59 ++ modules/mesh-build-go/buildstep/fetch_test.go | 137 +++ modules/mesh-build-go/buildstep/go.mod | 22 + modules/mesh-build-go/buildstep/go.sum | 27 + modules/mesh-build-go/buildstep/gotest.go | 410 +++++++++ modules/mesh-build-go/buildstep/guard.go | 52 ++ modules/mesh-build-go/buildstep/guard_test.go | 79 ++ modules/mesh-build-go/buildstep/isolate.go | 175 ++++ .../buildstep/isolate_caches_test.go | 30 + .../buildstep/isolate_integration_test.go | 66 ++ .../mesh-build-go/buildstep/isolate_test.go | 115 +++ modules/mesh-build-go/buildstep/merge.go | 128 +++ .../buildstep/merge_integration_test.go | 199 +++++ modules/mesh-build-go/buildstep/merge_test.go | 74 ++ .../buildstep/mirror_integration_test.go | 65 ++ .../mesh-build-go/buildstep/mirror_test.go | 124 +++ modules/mesh-build-go/buildstep/narrow.go | 72 ++ .../mesh-build-go/buildstep/narrow_test.go | 83 ++ modules/mesh-build-go/buildstep/pack.go | 100 +++ modules/mesh-build-go/buildstep/pack_test.go | 84 ++ .../mesh-build-go/buildstep/pack_unix_test.go | 25 + modules/mesh-build-go/buildstep/phase.go | 67 ++ modules/mesh-build-go/buildstep/programs.go | 115 +++ .../mesh-build-go/buildstep/programs_test.go | 72 ++ modules/mesh-build-go/buildstep/pull.go | 799 ++++++++++++++++++ modules/mesh-build-go/buildstep/pull_bus.go | 102 +++ .../buildstep/pull_capacity_test.go | 39 + modules/mesh-build-go/buildstep/pull_fake.go | 113 +++ .../buildstep/pull_integration_test.go | 144 ++++ modules/mesh-build-go/buildstep/pull_seat.go | 44 + .../mesh-build-go/buildstep/pull_seat_test.go | 40 + modules/mesh-build-go/buildstep/pull_test.go | 521 ++++++++++++ modules/mesh-build-go/buildstep/refusal.go | 29 + modules/mesh-build-go/buildstep/seat_tools.go | 39 + .../buildstep/seat_tools_test.go | 52 ++ modules/mesh-build-go/buildstep/step.go | 137 +++ modules/mesh-build-go/buildstep/step_test.go | 156 ++++ modules/mesh-build-go/buildstep/steptool.go | 36 + modules/mesh-build-go/buildstep/tap.go | 193 +++++ .../buildstep/testdata/isolated-probe/main.go | 49 ++ .../buildstep/testdata/pack-golden.tar | Bin 0 -> 4096 bytes modules/mesh-build-go/buildstep/trees.go | 116 +++ .../buildstep/trees_integration_test.go | 64 ++ .../buildstep/trees_named_test.go | 110 +++ modules/mesh-build-go/buildstep/trees_test.go | 96 +++ .../buildstep/watch_settings_linux.go | 112 +++ .../buildstep/watch_settings_other.go | 8 + modules/mesh-build-go/buildstep/workspaces.go | 160 ++++ .../buildstep/workspaces_test.go | 113 +++ .../mesh-build-go/cmd/mesh-build-go/asks.go | 13 +- .../mesh-build-go/asks_integration_test.go | 2 +- .../cmd/mesh-build-go/asks_test.go | 9 +- .../mesh-build-go/cmd/mesh-build-go/build.go | 2 +- .../cmd/mesh-build-go/compose.go | 5 +- .../mesh-build-go/compose_integration_test.go | 5 +- .../cmd/mesh-build-go/compose_test.go | 4 +- .../mesh-build-go/cmd/mesh-build-go/cpu.go | 4 +- .../mesh-build-go/cmd/mesh-build-go/fetch.go | 19 +- .../mesh-build-go/installed_toolchain_test.go | 2 +- .../cmd/mesh-build-go/integration.go | 4 +- .../cmd/mesh-build-go/isolate.go | 2 +- .../cmd/mesh-build-go/isolate_test.go | 2 +- .../cmd/mesh-build-go/kit_test.go | 10 +- .../mesh-build-go/cmd/mesh-build-go/level.go | 2 +- .../mesh-build-go/cmd/mesh-build-go/levels.go | 9 +- .../mesh-build-go/cmd/mesh-build-go/lint.go | 7 +- .../mesh-build-go/cmd/mesh-build-go/mirror.go | 2 +- .../cmd/mesh-build-go/mirror_test.go | 2 +- .../mesh-build-go/cmd/mesh-build-go/module.go | 7 +- .../cmd/mesh-build-go/packages_shape_test.go | 5 +- .../cmd/mesh-build-go/preview.go | 2 +- .../mesh-build-go/preview_integration_test.go | 2 +- .../mesh-build-go/cmd/mesh-build-go/push.go | 2 +- .../mesh-build-go/cmd/mesh-build-go/room.go | 2 +- .../cmd/mesh-build-go/room_test.go | 2 +- .../mesh-build-go/cmd/mesh-build-go/store.go | 2 +- .../mesh-build-go/cmd/mesh-build-go/test.go | 4 +- .../mesh-build-go/cmd/mesh-build-go/tools.go | 2 +- modules/mesh-build-go/go.mod | 3 + .../cmd/mesh-build-image/asks.go | 13 +- .../cmd/mesh-build-image/asks_test.go | 5 +- .../cmd/mesh-build-image/build.go | 2 +- .../cmd/mesh-build-image/copy.go | 2 +- .../cmd/mesh-build-image/cpu.go | 4 +- .../cmd/mesh-build-image/fetch.go | 9 +- .../cmd/mesh-build-image/integration_test.go | 2 +- .../cmd/mesh-build-image/kit_test.go | 6 +- .../cmd/mesh-build-image/module.go | 7 +- .../cmd/mesh-build-image/room.go | 2 +- .../cmd/mesh-build-image/room_test.go | 2 +- .../cmd/mesh-build-image/tools.go | 2 +- modules/mesh-build-image/go.mod | 3 + .../cmd/mesh-build-node/asks.go | 13 +- .../cmd/mesh-build-node/asks_test.go | 7 +- .../cmd/mesh-build-node/build.go | 7 +- .../cmd/mesh-build-node/build_test.go | 6 +- .../cmd/mesh-build-node/check.go | 13 +- .../cmd/mesh-build-node/check_test.go | 2 +- .../cmd/mesh-build-node/cpu.go | 4 +- .../cmd/mesh-build-node/fetch.go | 17 +- .../cmd/mesh-build-node/isolate.go | 2 +- .../cmd/mesh-build-node/isolate_test.go | 2 +- .../cmd/mesh-build-node/kit_test.go | 10 +- .../cmd/mesh-build-node/module.go | 7 +- .../cmd/mesh-build-node/package.go | 2 +- .../package_integration_test.go | 2 +- .../cmd/mesh-build-node/preview.go | 2 +- .../preview_integration_test.go | 2 +- .../cmd/mesh-build-node/push.go | 2 +- .../cmd/mesh-build-node/room.go | 2 +- .../cmd/mesh-build-node/room_test.go | 2 +- .../cmd/mesh-build-node/tools.go | 2 +- modules/mesh-build-node/go.mod | 3 + 159 files changed, 9941 insertions(+), 139 deletions(-) create mode 100644 modules/mesh-build-go/buildstep/askrun.go create mode 100644 modules/mesh-build-go/buildstep/askrun_test.go create mode 100644 modules/mesh-build-go/buildstep/blob.go create mode 100644 modules/mesh-build-go/buildstep/blob_test.go create mode 100644 modules/mesh-build-go/buildstep/buildcpu/buildcpu.go create mode 100644 modules/mesh-build-go/buildstep/buildcpu/buildcpu_test.go create mode 100644 modules/mesh-build-go/buildstep/buildhours.go create mode 100644 modules/mesh-build-go/buildstep/capacity.go create mode 100644 modules/mesh-build-go/buildstep/capacity_cpu.go create mode 100644 modules/mesh-build-go/buildstep/capacity_test.go create mode 100644 modules/mesh-build-go/buildstep/cmd/mesh-pack/main.go create mode 100644 modules/mesh-build-go/buildstep/cmd/mesh-pack/main_test.go create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/README.md create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/buildfail.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/deeptimeout.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/exiting.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/killed.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/logthenfatal.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/mainexit.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/panicking.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/passfirst.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/passthenfail.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/race.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/racemain.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/subpanic.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/subtests.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/go/timeout.jsonl create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/crash.console create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/crash.tap create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/logcrash.console create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/logcrash.tap create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/suite.console create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/suite.tap create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/syntax.console create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/syntax.tap create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/timeout.console create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/timeout.tap create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/two-files.console create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/node/two-files.tap create mode 100644 modules/mesh-build-go/buildstep/conformance/captured/tsc/typeerror.console create mode 100644 modules/mesh-build-go/buildstep/conformance/conformance.go create mode 100644 modules/mesh-build-go/buildstep/conformance/conformance_test.go create mode 100644 modules/mesh-build-go/buildstep/conformance/leftrunning.go create mode 100644 modules/mesh-build-go/buildstep/cpu.go create mode 100644 modules/mesh-build-go/buildstep/digestshape_test.go create mode 100644 modules/mesh-build-go/buildstep/fetch.go create mode 100644 modules/mesh-build-go/buildstep/fetch_integration_test.go create mode 100644 modules/mesh-build-go/buildstep/fetch_test.go create mode 100644 modules/mesh-build-go/buildstep/go.mod create mode 100644 modules/mesh-build-go/buildstep/go.sum create mode 100644 modules/mesh-build-go/buildstep/gotest.go create mode 100644 modules/mesh-build-go/buildstep/guard.go create mode 100644 modules/mesh-build-go/buildstep/guard_test.go create mode 100644 modules/mesh-build-go/buildstep/isolate.go create mode 100644 modules/mesh-build-go/buildstep/isolate_caches_test.go create mode 100644 modules/mesh-build-go/buildstep/isolate_integration_test.go create mode 100644 modules/mesh-build-go/buildstep/isolate_test.go create mode 100644 modules/mesh-build-go/buildstep/merge.go create mode 100644 modules/mesh-build-go/buildstep/merge_integration_test.go create mode 100644 modules/mesh-build-go/buildstep/merge_test.go create mode 100644 modules/mesh-build-go/buildstep/mirror_integration_test.go create mode 100644 modules/mesh-build-go/buildstep/mirror_test.go create mode 100644 modules/mesh-build-go/buildstep/narrow.go create mode 100644 modules/mesh-build-go/buildstep/narrow_test.go create mode 100644 modules/mesh-build-go/buildstep/pack.go create mode 100644 modules/mesh-build-go/buildstep/pack_test.go create mode 100644 modules/mesh-build-go/buildstep/pack_unix_test.go create mode 100644 modules/mesh-build-go/buildstep/phase.go create mode 100644 modules/mesh-build-go/buildstep/programs.go create mode 100644 modules/mesh-build-go/buildstep/programs_test.go create mode 100644 modules/mesh-build-go/buildstep/pull.go create mode 100644 modules/mesh-build-go/buildstep/pull_bus.go create mode 100644 modules/mesh-build-go/buildstep/pull_capacity_test.go create mode 100644 modules/mesh-build-go/buildstep/pull_fake.go create mode 100644 modules/mesh-build-go/buildstep/pull_integration_test.go create mode 100644 modules/mesh-build-go/buildstep/pull_seat.go create mode 100644 modules/mesh-build-go/buildstep/pull_seat_test.go create mode 100644 modules/mesh-build-go/buildstep/pull_test.go create mode 100644 modules/mesh-build-go/buildstep/refusal.go create mode 100644 modules/mesh-build-go/buildstep/seat_tools.go create mode 100644 modules/mesh-build-go/buildstep/seat_tools_test.go create mode 100644 modules/mesh-build-go/buildstep/step.go create mode 100644 modules/mesh-build-go/buildstep/step_test.go create mode 100644 modules/mesh-build-go/buildstep/steptool.go create mode 100644 modules/mesh-build-go/buildstep/tap.go create mode 100644 modules/mesh-build-go/buildstep/testdata/isolated-probe/main.go create mode 100644 modules/mesh-build-go/buildstep/testdata/pack-golden.tar create mode 100644 modules/mesh-build-go/buildstep/trees.go create mode 100644 modules/mesh-build-go/buildstep/trees_integration_test.go create mode 100644 modules/mesh-build-go/buildstep/trees_named_test.go create mode 100644 modules/mesh-build-go/buildstep/trees_test.go create mode 100644 modules/mesh-build-go/buildstep/watch_settings_linux.go create mode 100644 modules/mesh-build-go/buildstep/watch_settings_other.go create mode 100644 modules/mesh-build-go/buildstep/workspaces.go create mode 100644 modules/mesh-build-go/buildstep/workspaces_test.go diff --git a/modules/mesh-build-go/buildstep/askrun.go b/modules/mesh-build-go/buildstep/askrun.go new file mode 100644 index 00000000..2a0836b2 --- /dev/null +++ b/modules/mesh-build-go/buildstep/askrun.go @@ -0,0 +1,473 @@ +package buildstep + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "path" + "slices" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// **One ask, one whole job** (novox/hq ADR 0525 §3). A build module runs a pulled ask with its own step tools' bodies, +// the same ones its request and reply tools run (ADR 0525 §8), in the order the job names: a build ask fetches, then +// builds or copies each artifact; a check ask fetches, digests the level, takes the kept verdicts and tests the rest, +// the packages whose tests the change touches first. The answers are the tools' own, whole, so the asker judges them as +// it judged the tools' answers. + +// StepFunc is one step tool's body: its arguments in, its answer out. +type StepFunc func(ctx context.Context, args map[string]any) (any, error) + +// AskSteps are the step tools a build module runs an ask with. Copy is the image module's alone; Digest the Go module's. +type AskSteps struct { + Fetch, Build, Copy, Digest, Test StepFunc + // Toolchain is the toolchain the module builds with now; a build ask naming another is declined (Declining). Nil checks nothing + // (an image or an archive names none). + Toolchain func(ctx context.Context) (delivery.ToolchainUsed, error) + // Kept looks up a kept verdict by level and digest (the checker's kept bucket, delivery.KeptKey): whether one is + // kept and how many tests it counted. Nil takes none. + Kept func(ctx context.Context, level, digest string) (tests int, found bool, err error) + // Release removes a workspace once the ask's steps on it ended (the module's release tool's body); nil leaves it to + // the module's sweep. + Release func(workspace string) error + Logf func(format string, args ...any) + // Has says whether this node has a need a package's tests declare (delivery.AskNeeds, issue 890); a package needing + // what it lacks is not tested here but named in the level's Unmet, for the asker to place. Nil has nothing. + Has func(need string) bool +} + +// RunAsk runs ask's job with steps and answers a delivery.BuildAnswer or a delivery.CheckAnswer. +func RunAsk(ctx context.Context, steps AskSteps, ask delivery.StepAsk) (any, error) { + if ask.Build != nil { + job := *ask.Build + job.Fetch = withPackages(job.Fetch, ask.Packages) + return steps.build(ctx, job) + } + if ask.Check != nil { + job := *ask.Check + job.Fetch = withPackages(job.Fetch, ask.Packages) + return steps.check(ctx, job) + } + return nil, Refuse("the ask %s holds no job", ask.ID) +} + +// withPackages is the fetch's arguments with the ask's package versions (novox/hq ADR 0573 §2), which the build module +// writes into the workspace it fetches; a copy, so the ask is left as it was. An ask with none, or a copy alone that +// fetches nothing, is left as it is. +func withPackages(fetch map[string]any, packages delivery.PackageVersions) map[string]any { + if len(packages) == 0 || fetch == nil { + return fetch + } + out := maps.Clone(fetch) + out["packages"] = maps.Clone(packages) + return out +} + +func raw(v any) (json.RawMessage, error) { + if r, ok := v.(json.RawMessage); ok { + return r, nil + } + return json.Marshal(v) +} + +// fetched is what an ask reads of a fetch's answer. +type fetched struct { + Workspace string `json:"workspace"` + Merge string `json:"merge"` + Conflicts []string `json:"conflicts"` + Changed []string `json:"changed"` + Modules []string `json:"modules"` +} + +func (s AskSteps) fetch(ctx context.Context, args map[string]any) (json.RawMessage, fetched, error) { + if s.Fetch == nil { + return nil, fetched{}, Refuse("this build module fetches nothing") + } + if _, merges := args["base"]; merges { + Phase(ctx, "fetch and merge") + } else { + Phase(ctx, "fetch") + } + out, err := s.Fetch(ctx, args) + if err != nil { + return nil, fetched{}, err + } + r, err := raw(out) + if err != nil { + return nil, fetched{}, err + } + var f fetched + if err := json.Unmarshal(r, &f); err != nil || f.Workspace == "" { + return r, f, fmt.Errorf("the fetch answered no workspace: %s", r) + } + return r, f, nil +} + +// onToolchain is the toolchain this node builds with when the ask names one and this module reports its own, nil when +// either is absent. A node on another toolchain than the ask names answers a Declining: it is not on the agreed one and +// sits out, for a node that is to take the ask (novox/hq ADR 0584 clause 2). +func (s AskSteps) onToolchain(ctx context.Context, want *delivery.ToolchainUsed) (*delivery.ToolchainUsed, error) { + if want == nil || s.Toolchain == nil { + return nil, nil + } + tc, err := s.Toolchain(ctx) + if err != nil { + return nil, err + } + if tc.Version != want.Version || tc.Recipe != want.Recipe { + return nil, &Declining{Lacks: []string{fmt.Sprintf("the agreed toolchain %s (recipe %s)", want.Version, want.Recipe)}, + Text: fmt.Sprintf("the ask is for %s (recipe %s) and this node builds with %s (recipe %s): it sits out, and the ask is left to a node on the agreed toolchain", + want.Version, want.Recipe, tc.Version, tc.Recipe)} + } + return &tc, nil +} + +func (s AskSteps) build(ctx context.Context, job delivery.BuildJob) (any, error) { + answer := delivery.BuildAnswer{} + tc, err := s.onToolchain(ctx, job.Toolchain) + if err != nil { + return nil, err + } + answer.Toolchain = tc + var f fetched + defer func() { s.release(f.Workspace) }() + for _, a := range job.Artifacts { + if a.From == "" && f.Workspace == "" { + r, got, err := s.fetch(ctx, job.Fetch) + if err != nil { + return nil, err + } + if _, candidate := job.Fetch["base"]; candidate { + // A candidate is built from its head merged onto the trunk; a head that does not merge leaves the tree at the + // trunk, and building that would store the trunk's build under the candidate's fingerprint. + if len(got.Conflicts) > 0 { + return nil, fmt.Errorf("the head does not merge into the trunk: files in conflict: %s", strings.Join(got.Conflicts, ", ")) + } + if got.Merge == "" { + return nil, fmt.Errorf("the fetch with a base answered no merge: %s", r) + } + } + f = got + } + var ( + out any + err error + ) + if a.From != "" { + if s.Copy == nil { + return nil, Refuse("this build module copies no artifact (%s)", a.Artifact) + } + Phase(ctx, "copy "+a.Artifact) + out, err = s.Copy(ctx, map[string]any{"module": job.Module, "artifact": a.Artifact, "from": a.From}) + } else { + if s.Build == nil { + return nil, Refuse("this build module builds no artifact (%s)", a.Artifact) + } + args := map[string]any{"workspace": f.Workspace, "module": job.Path, "artifact": a.Artifact} + if job.Publish { + args["publish"] = true + } + Phase(ctx, "build "+a.Artifact) + out, err = s.Build(ctx, args) + } + if err != nil { + return nil, fmt.Errorf("%s: %w", a.Artifact, err) + } + r, err := raw(out) + if err != nil { + return nil, err + } + var built struct { + Digest string `json:"digest"` + Reference string `json:"reference"` + } + if err := json.Unmarshal(r, &built); err != nil || built.Digest == "" { + return nil, fmt.Errorf("the build of %s answered no digest: %s", a.Artifact, r) + } + answer.Artifacts = append(answer.Artifacts, delivery.AskBuilt{Artifact: a.Artifact, Digest: built.Digest, Reference: built.Reference}) + } + return answer, nil +} + +func (s AskSteps) check(ctx context.Context, job delivery.CheckJob) (any, error) { + if _, err := s.onToolchain(ctx, job.Toolchain); err != nil { + return nil, err + } + r, f, err := s.fetch(ctx, job.Fetch) + if err != nil { + return nil, err + } + defer s.release(f.Workspace) + answer := delivery.CheckAnswer{Fetch: r} + if len(f.Conflicts) > 0 { + return answer, nil // the asker reads the conflict from the fetch's answer: a verdict, not an error + } + if s.Test == nil { + return nil, Refuse("this build module tests nothing") + } + runner := "" + if job.Runner != "" && (job.RunnerWhen == "" || changesUnder(f.Changed, job.RunnerWhen)) { + runner = job.Runner + } + // Every level runs, also after one that did not pass: one check names every failure, not only the first (the operator, + // 2026-11-02: "A failed check shows only its first fault"). + for _, level := range job.Levels { + la := delivery.LevelAnswer{Level: level} + _, err := s.level(ctx, job, level, f, runner, &la) + answer.Levels = append(answer.Levels, la) + if err != nil { + return nil, err + } + } + return answer, nil +} + +// level runs one level of a check ask and says whether it passed. +func (s AskSteps) level(ctx context.Context, job delivery.CheckJob, level string, f fetched, runner string, la *delivery.LevelAnswer) (bool, error) { + withRunner := func(args map[string]any) map[string]any { + if runner != "" { + args["runner"] = runner + } + if len(job.NodeNames) > 0 { + args[delivery.NodeNamesArg] = job.NodeNames // the names check's (ADR 0567 §12) + } + return args + } + test := func(args map[string]any) (bool, error) { + what := "the whole level" + if dir, _ := args["module"].(string); dir != "" { + what = dir + } else if pkgs, _ := args["packages"].([]string); len(pkgs) > 0 { + what = fmt.Sprintf("%d packages", len(pkgs)) + } + Phase(ctx, fmt.Sprintf("%s: tests of %s", level, what)) + out, err := s.Test(ctx, args) + if err != nil { + return false, err + } + r, err := raw(out) + if err != nil { + return false, err + } + la.Tests = append(la.Tests, r) + var t struct { + Passed bool `json:"passed"` + } + _ = json.Unmarshal(r, &t) + return t.Passed, nil + } + // The packages (or Node directories) whose tests the change touches run first (issue 1289; ADR 0444), and then every + // other test step of the level, also after one that did not pass, so one check names every failing test of the level + // and not only the first (issue 1921; the operator, 2026-11-02); the level passes only when each of its steps did. + all := true + if s.Digest == nil { + // A module with no digest (Node) tests each module directory of the tree, those whose tests changed first. + for _, priority := range []bool{true, false} { + for _, dir := range f.Modules { + if changesTestIn(f.Changed, dir) != priority { + continue + } + passed, err := test(withRunner(map[string]any{"workspace": f.Workspace, "module": dir, "level": level})) + if err != nil { + return false, fmt.Errorf("the %s level of %s: %w", level, dir, err) + } + all = all && passed + } + } + return all, nil + } + todo, whole, keepRunner := s.plan(ctx, job, level, f, runner, withRunner, la) + if !keepRunner { + runner = "" // an installed module that ignores the runner judges the level as it judges any other (ADR 0535 §2) + } + if whole && len(job.Packages) > 0 { + todo, whole = job.Packages, false // a placed ask tests its packages alone, whatever the digest said (ADR 0552 §4) + } + for _, priority := range []bool{true, false} { + args := withRunner(map[string]any{"workspace": f.Workspace, "level": level}) + if job.Run != "" { + args["run"] = job.Run + } + if whole { + if priority { + continue // nothing to put first: the level runs whole after + } + } else { + var run []string + for _, p := range todo { + if changesTestOf(f.Changed, p) == priority { + run = append(run, p) + } + } + if len(run) == 0 { + continue + } + args["packages"] = run + } + passed, err := test(args) + if err != nil { + return false, fmt.Errorf("the %s level: %w", level, err) + } + all = all && passed // the others still run after a failure (issue 1921) + } + return all, nil +} + +// release removes an ask's workspace, saying a failure: the module's sweep removes it later. +func (s AskSteps) release(workspace string) { + if workspace == "" || s.Release == nil { + return + } + if err := s.Release(workspace); err != nil && s.Logf != nil { + s.Logf("the workspace %s is left to the sweep: %v", workspace, err) + } +} + +// plan digests the level and takes the kept verdicts: the packages left to test, or all when the level runs whole, and +// whether the runner stays. A try of some tests runs whole, digesting nothing (a partial run proves no package). Under the +// candidate's runner the digest is asked as the candidate: when the candidate answers it, every package runs and nothing +// is digested for keeping (ADR 0535 §4); when the installed module answers it (it does not take the runner), the level is +// planned as any other and the runner is dropped. The level runs whole and takes nothing when the digest names no package +// or does not cover the siblings the fetch placed (ADR 0444, L5). +func (s AskSteps) plan(ctx context.Context, job delivery.CheckJob, level string, f fetched, runner string, withRunner func(map[string]any) map[string]any, la *delivery.LevelAnswer) ([]string, bool, bool) { + if job.Run != "" { + return nil, true, true + } + Phase(ctx, level+": digest") + out, err := s.Digest(ctx, withRunner(map[string]any{"workspace": f.Workspace, "level": level})) + if err != nil { + if s.Logf != nil { + s.Logf("no digest of the %s level, so it runs whole: %v", level, err) + } + return nil, true, true + } + r, err := raw(out) + if err != nil { + return nil, true, true + } + var d delivery.LevelDigest // packages kept as a sorted list, never keyed by a package path (issue 1930) + if err := json.Unmarshal(r, &d); err != nil { + return nil, true, true + } + if runner != "" { + if strings.HasPrefix(d.JudgedBy, "candidate ") { + la.Judged = d.JudgedBy + return nil, true, true // the candidate decides which verdicts would be taken: all run, nothing kept + } + runner = "" + } + if len(d.Packages) == 0 || !slices.Equal(d.Beside, besideOf(job.Fetch)) { + return nil, true, runner != "" // a digest that names nothing or not the siblings keys no verdict: the level runs whole + } + la.Digest = &d + only := map[string]bool{} + for _, p := range job.Packages { + only[p] = true + } + var todo []string + for _, pd := range d.Packages { + p := pd.Package + if len(only) > 0 && !only[p] { + continue // a placed ask's other packages were judged by the first ask (ADR 0552 §4) + } + if job.Kept && s.Kept != nil { + tests, found, err := s.Kept(ctx, level, pd.Digest) + if err != nil && s.Logf != nil { + s.Logf("the kept verdict of %s at %s cannot be read, so it runs: %v", p, level, err) + } + if err == nil && found { + la.Kept = append(la.Kept, delivery.KeptTaken{Package: p, Digest: pd.Digest, Tests: tests}) + continue + } + } + if needs := d.NeedsOf(p); len(delivery.Lacks(needs, s.Has)) > 0 { + // Its tests would only skip here (issue 890): it is left for a node that has what they need. + la.Unmet = append(la.Unmet, delivery.PackageNeeds{Package: p, Needs: needs}) + if s.Logf != nil { + s.Logf("the %s level of %s is left to a node with %s: this node lacks %s", level, p, + strings.Join(needs, ", "), strings.Join(delivery.Lacks(needs, s.Has), ", ")) + } + continue + } + todo = append(todo, p) + } + return todo, false, false +} + +// besideOf are the siblings a fetch's arguments place, as a digest echoes them: ` `, sorted. +func besideOf(fetch map[string]any) []string { + raw, err := json.Marshal(fetch["beside"]) + if err != nil { + return nil + } + var refs []struct { + Repository string `json:"repository"` + Commit string `json:"commit"` + } + _ = json.Unmarshal(raw, &refs) + var out []string + for _, r := range refs { + out = append(out, r.Repository+" "+r.Commit) + } + sort.Strings(out) + return out +} + +// changesUnder says whether a path the merge changes is under prefix. +func changesUnder(changed []string, prefix string) bool { + for _, p := range changed { + if strings.HasPrefix(p, prefix) { + return true + } + } + return false +} + +// isTestFile says whether a repository-relative path is a test file: Go's _test.go, and a TypeScript module's .test. +// and .spec. files and test directories. +func isTestFile(p string) bool { + switch { + case strings.HasSuffix(p, "_test.go"): + return true + case strings.Contains(p, ".test.") || strings.Contains(p, ".spec."): + return true + } + for _, seg := range strings.Split(path.Dir(p), "/") { + if seg == "__tests__" || seg == "test" || seg == "tests" { + return true + } + } + return false +} + +// changesTestOf says whether the merge changes a test file of the Go package with the import path pkg: the file's +// directory and the import path agree on their last segments. +func changesTestOf(changed []string, pkg string) bool { + for _, f := range changed { + if !strings.HasSuffix(f, "_test.go") { + continue + } + dir := path.Dir(f) + if dir == pkg || strings.HasSuffix(pkg, "/"+dir) || strings.HasSuffix(dir, "/"+pkg) { + return true + } + } + return false +} + +// changesTestIn says whether the merge changes a test file inside the directory of a Node module. +func changesTestIn(changed []string, dir string) bool { + for _, f := range changed { + if !isTestFile(f) || strings.HasSuffix(f, "_test.go") { + continue + } + if dir == "." || dir == "" || strings.HasPrefix(f, strings.TrimSuffix(dir, "/")+"/") { + return true + } + } + return false +} diff --git a/modules/mesh-build-go/buildstep/askrun_test.go b/modules/mesh-build-go/buildstep/askrun_test.go new file mode 100644 index 00000000..7fe442c3 --- /dev/null +++ b/modules/mesh-build-go/buildstep/askrun_test.go @@ -0,0 +1,326 @@ +package buildstep + +import ( + "context" + "encoding/json" + "errors" + "fmt" + sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep" + "slices" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +const runCommit = "0123456789abcdef0123456789abcdef01234567" + +// recorder is a build module's step tools that record each call. +type recorder struct { + calls []string + fetch map[string]any + tests map[string]bool // package or module directory -> passed + digests map[string]string + needs []delivery.PackageNeeds +} + +func (r *recorder) steps() AskSteps { + call := func(name string, answer func(args map[string]any) any) StepFunc { + return func(ctx context.Context, args map[string]any) (any, error) { + b, _ := json.Marshal(args) + r.calls = append(r.calls, name+" "+string(b)) + return answer(args), nil + } + } + return AskSteps{ + Fetch: call("fetch", func(map[string]any) any { return r.fetch }), + Build: call("build", func(args map[string]any) any { + return map[string]any{"digest": "sha256:" + args["artifact"].(string), "reference": "artifact-store://" + args["artifact"].(string)} + }), + Copy: call("copy", func(args map[string]any) any { return map[string]any{"digest": "sha256:copied"} }), + Digest: call("digest", func(map[string]any) any { + return map[string]any{"level": "unit", "packages": r.digests, "needs": r.needs} + }), + Test: call("test", func(args map[string]any) any { + passed := true + if pkgs, ok := args["packages"].([]string); ok { + for _, p := range pkgs { + passed = passed && r.tests[p] + } + } + if dir, ok := args["module"].(string); ok { + passed = r.tests[dir] + } + return map[string]any{"passed": passed, "tests": 1} + }), + Kept: func(ctx context.Context, level, digest string) (int, bool, error) { return 7, digest == "d-kept", nil }, + } +} + +func ask(job any) delivery.StepAsk { + a := delivery.StepAsk{ID: sdkstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: "t", At: time.Now()} + switch j := job.(type) { + case delivery.BuildJob: + a.Build = &j + case delivery.CheckJob: + a.Check = &j + } + return a +} + +// 1887/A3: a build ask fetches once, builds each artifact from the workspace with publish, copies a pinned one without a +// fetch, and answers each artifact's digest and reference. +func TestRunAskBuilds(t *testing.T) { + r := &recorder{fetch: map[string]any{"workspace": "/w/1"}} + steps := r.steps() + var released []string + steps.Release = func(ws string) error { released = append(released, ws); return nil } + // Issue 1585: the ask's package versions reach the fetch, and the ask's own fetch arguments stay as they were. + a := ask(delivery.BuildJob{Module: "m", Path: "modules/m", Publish: true, + Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}, + Artifacts: []delivery.AskArtifact{{Artifact: "base", From: "registry/b@sha256:00"}, {Artifact: "bundle"}, {Artifact: "lib"}}}) + a.Packages = delivery.PackageVersions{"git.novox.be/novox/mesh-sdk/go": runCommit} + out, err := RunAsk(context.Background(), steps, a) + if err != nil { + t.Fatal(err) + } + if _, put := a.Build.Fetch["packages"]; put { + t.Error("the ask's fetch arguments were changed") + } + got := strings.Join(r.calls, "\n") + want := `copy {"artifact":"base","from":"registry/b@sha256:00","module":"m"} +fetch {"commit":"` + runCommit + `","packages":{"git.novox.be/novox/mesh-sdk/go":"` + runCommit + `"},"repository":"novox/x"} +build {"artifact":"bundle","module":"modules/m","publish":true,"workspace":"/w/1"} +build {"artifact":"lib","module":"modules/m","publish":true,"workspace":"/w/1"}` + if got != want { + t.Fatalf("calls:\n%s\nwant:\n%s", got, want) + } + if len(released) != 1 || released[0] != "/w/1" { + t.Errorf("released %v, want the one workspace", released) + } + answer := out.(delivery.BuildAnswer) + if len(answer.Artifacts) != 3 || answer.Artifacts[1].Reference != "artifact-store://bundle" { + t.Fatalf("answer %+v", answer) + } +} + +// A build ask for another toolchain is declined (ADR 0584 clause 2), so the node sits out and the asker puts it again. +func TestRunAskRefusesAnotherToolchain(t *testing.T) { + r := &recorder{fetch: map[string]any{"workspace": "/w"}} + s := r.steps() + s.Toolchain = func(context.Context) (delivery.ToolchainUsed, error) { + return delivery.ToolchainUsed{Version: "go1.27", Recipe: "3"}, nil + } + _, err := RunAsk(context.Background(), s, ask(delivery.BuildJob{Module: "m", Path: ".", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}, + Artifacts: []delivery.AskArtifact{{Artifact: "bundle"}}, Toolchain: &delivery.ToolchainUsed{Version: "go1.26", Recipe: "3"}})) + var dec *Declining + if !errors.As(err, &dec) || len(dec.Lacks) != 1 || len(r.calls) != 0 { + t.Fatalf("err %v, calls %v", err, r.calls) + } + // A check ask names the toolchain too (ADR 0584), and a node on another declines it before it fetches. + _, err = RunAsk(context.Background(), s, ask(delivery.CheckJob{Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}, Levels: []string{delivery.LevelUnit}, + Toolchain: &delivery.ToolchainUsed{Version: "go1.26", Recipe: "3"}})) + if !errors.As(err, &dec) || len(r.calls) != 0 { + t.Fatalf("a check ask: err %v, calls %v", err, r.calls) + } +} + +// 1887/A3, A5a: a check ask runs the levels of one check after one fetch: it digests the unit level, takes the kept +// verdicts, tests the packages whose tests changed first and then the others, also after one of them failed, and runs +// every level, also after one that did not pass, so one check names every failure (the operator, 2026-11-02). +func TestRunAskChecksTheLevelsOfOneCheck(t *testing.T) { + r := &recorder{ + fetch: map[string]any{"workspace": "/w", "changed": []string{"pkg/b/b_test.go"}}, + digests: map[string]string{"x/pkg/a": "d-a", "x/pkg/b": "d-b", "x/pkg/k": "d-kept"}, + tests: map[string]bool{"x/pkg/a": true, "x/pkg/b": false}, + } + out, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit, delivery.LevelIntegration}, Kept: true, + Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit, "base": runCommit}})) + if err != nil { + t.Fatal(err) + } + got := strings.Join(r.calls, "\n") + want := `fetch {"base":"` + runCommit + `","commit":"` + runCommit + `","repository":"novox/x"} +digest {"level":"unit","workspace":"/w"} +test {"level":"unit","packages":["x/pkg/b"],"workspace":"/w"} +test {"level":"unit","packages":["x/pkg/a"],"workspace":"/w"} +digest {"level":"integration","workspace":"/w"} +test {"level":"integration","packages":["x/pkg/b"],"workspace":"/w"} +test {"level":"integration","packages":["x/pkg/a"],"workspace":"/w"}` + if got != want { + t.Fatalf("calls:\n%s\nwant:\n%s", got, want) + } + a := out.(delivery.CheckAnswer) + if len(a.Levels) != 2 { + t.Fatalf("levels %+v, want both levels", a.Levels) + } + u := a.Levels[0] + if u.Level != delivery.LevelUnit || len(u.Kept) != 1 || u.Kept[0].Package != "x/pkg/k" || u.Kept[0].Tests != 7 || len(u.Tests) != 2 || u.Digest == nil { + t.Fatalf("the unit level %+v", u) + } + + // ADR 0567 §12: a check's node names reach the digest and test tools, for the names check (a kept verdict misses when + // they change). + r.calls = nil + if _, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, NodeNames: []string{"anchor"}, + Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})); err != nil || !strings.Contains(strings.Join(r.calls, "\n"), `digest {"level":"unit","node-names":["anchor"]`) || + !strings.Contains(strings.Join(r.calls, "\n"), `test {"level":"unit","node-names":["anchor"]`) { + t.Fatalf("the node names are not handed to the digest and test tools: %v, %v", r.calls, err) + } + + // Issue 890/AC2: a package declaring node-build-node is not tested on a node that lacks it but named unmet, and is + // tested on a node that has it. + for _, has := range []bool{false, true} { + r.calls, r.tests["x/pkg/b"] = nil, true + r.needs = []delivery.PackageNeeds{{Package: "x/pkg/a", Needs: []string{delivery.NeedNodeBuildNode}}} + steps := r.steps() + if has { + steps.Has = func(n string) bool { return n == delivery.NeedNodeBuildNode } + } + out, err := RunAsk(context.Background(), steps, ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true, + Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})) + if err != nil { + t.Fatal(err) + } + u := out.(delivery.CheckAnswer).Levels[0] + tested := strings.Contains(strings.Join(r.calls, "\n"), `"x/pkg/a"`) + if tested != has || (len(u.Unmet) == 0) == !has || (!has && (u.Unmet[0].Package != "x/pkg/a" || delivery.UnmetNeeds([]delivery.LevelAnswer{u})[0] != delivery.NeedNodeBuildNode)) { + t.Fatalf("has %v: tested x/pkg/a %v, unmet %+v, calls %v", has, tested, u.Unmet, r.calls) + } + } + // A placed ask naming its packages tests those alone and takes no other's kept verdict, also when the level would + // run whole (ADR 0552 §4). + for _, digests := range []map[string]string{r.digests, nil} { + r.calls, r.digests = nil, digests + steps := r.steps() + steps.Has = func(string) bool { return true } + out, err := RunAsk(context.Background(), steps, ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true, Packages: []string{"x/pkg/a"}, + Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})) + if err != nil { + t.Fatal(err) + } + u := out.(delivery.CheckAnswer).Levels[0] + if last := r.calls[len(r.calls)-1]; !strings.HasSuffix(last, `"packages":["x/pkg/a"],"workspace":"/w"}`) || len(u.Kept) != 0 || len(u.Tests) != 1 { + t.Fatalf("digests %v: calls %v, kept %+v: want x/pkg/a alone tested, nothing taken kept", digests != nil, r.calls, u.Kept) + } + } +} + +// ADR 0535 §2, §4: a merge that changes the build module's own code is judged by the candidate's runner: its digest says +// the candidate ran, and every package runs with nothing kept; an installed module that ignores the runner is planned as +// any other, the runner dropped. A digest that does not cover the fetch's siblings runs the level whole too. +func TestRunAskRunsWholeUnderTheRunnerOrWithoutTheSiblings(t *testing.T) { + for _, tc := range []struct{ judged, want string }{ + {"candidate 999", `digest {"level":"unit","runner":"workspace","workspace":"/w"} +test {"level":"unit","runner":"workspace","workspace":"/w"}`}, + {"installed", `digest {"level":"unit","runner":"workspace","workspace":"/w"}`}, + } { + r := &recorder{fetch: map[string]any{"workspace": "/w", "changed": []string{"modules/mesh-build-go/x.go"}}, + digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}} + steps := r.steps() + digest := steps.Digest + steps.Digest = func(ctx context.Context, args map[string]any) (any, error) { + _, _ = digest(ctx, args) + return map[string]any{"level": "unit", "packages": r.digests, "judged_by": tc.judged}, nil + } + out, err := RunAsk(context.Background(), steps, ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true, + Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})) + if err != nil { + t.Fatal(err) + } + if got := strings.Join(r.calls[1:], "\n"); got != tc.want { + t.Errorf("judged by %s: calls\n%s\nwant\n%s", tc.judged, got, tc.want) + } + l := out.(delivery.CheckAnswer).Levels[0] + if tc.judged == "installed" && (len(l.Kept) != 1 || l.Judged != "") { + t.Errorf("an installed module's level %+v, want the kept verdict taken", l) + } + if tc.judged != "installed" && (len(l.Kept) != 0 || l.Digest != nil || l.Judged != tc.judged) { + t.Errorf("the candidate's level %+v, want nothing kept and the program named", l) + } + } + r := &recorder{fetch: map[string]any{"workspace": "/w"}, digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}} + out, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true, + Fetch: map[string]any{"repository": "novox/x", "commit": runCommit, "beside": []any{map[string]any{"repository": "novox/mesh-sdk", "commit": runCommit}}}})) + if err != nil { + t.Fatal(err) + } + if l := out.(delivery.CheckAnswer).Levels[0]; len(l.Kept) != 0 || l.Digest != nil { + t.Fatalf("a digest without the siblings kept %+v", l) + } +} + +// A Node check tests each module directory, the one whose tests changed first; a conflict ends the ask with the fetch +// alone, a verdict for the asker. +func TestRunAskChecksNodeModulesAndConflicts(t *testing.T) { + r := &recorder{fetch: map[string]any{"workspace": "/w", "modules": []string{"a", "b"}, "changed": []string{"b/test/x.test.ts"}}, + tests: map[string]bool{"a": true, "b": true}} + s := r.steps() + s.Digest = nil + if _, err := RunAsk(context.Background(), s, ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})); err != nil { + t.Fatal(err) + } + if fmt.Sprint(r.calls[1:]) != `[test {"level":"unit","module":"b","workspace":"/w"} test {"level":"unit","module":"a","workspace":"/w"}]` { + t.Fatalf("calls %v", r.calls) + } + r = &recorder{fetch: map[string]any{"workspace": "/w", "conflicts": []string{"go.mod"}}} + out, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})) + if err != nil || len(r.calls) != 1 || len(out.(delivery.CheckAnswer).Levels) != 0 { + t.Fatalf("a conflict: %v %v", err, r.calls) + } +} + +// Issue 1289: the packages whose tests changed run first. Since 2026-11-02 (the operator: "A failed check shows only its +// first fault") a failure among them ends nothing: every package of the level and every level still runs, so one check +// names every failing test (issue 1921). A Node check does the same with its module directories. +func TestRunAskRunsEveryPackageAndLevelAfterAFailure(t *testing.T) { + for _, row := range []struct { + name string + changed []string + tests map[string]bool + want []string // the packages tested at each level, in order + }{ + {"the change's own test fails: the rest still runs", []string{"pkg/b/b_test.go"}, map[string]bool{"x/pkg/a": false, "x/pkg/b": false}, []string{"x/pkg/b", "x/pkg/a"}}, + {"the change's own test passes: the rest runs, also after a failure", []string{"pkg/b/b_test.go"}, map[string]bool{"x/pkg/a": false, "x/pkg/b": true, "x/pkg/c": false}, []string{"x/pkg/b", "x/pkg/a x/pkg/c"}}, + {"no test changed: every failure is named", nil, map[string]bool{"x/pkg/a": false, "x/pkg/b": false}, []string{"x/pkg/a x/pkg/b"}}, + } { + r := &recorder{fetch: map[string]any{"workspace": "/w", "changed": row.changed}, tests: row.tests, + digests: map[string]string{"x/pkg/a": "d-a", "x/pkg/b": "d-b"}} + if _, ok := row.tests["x/pkg/c"]; ok { + r.digests["x/pkg/c"] = "d-c" + } + out, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit, delivery.LevelIntegration}, + Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})) + if err != nil { + t.Fatal(err) + } + var got []string + for _, c := range r.calls[1:] { + if strings.HasPrefix(c, "test ") { + var a struct{ Packages []string } + _ = json.Unmarshal([]byte(strings.TrimPrefix(c, "test ")), &a) + got = append(got, strings.Join(a.Packages, " ")) + } + } + if want := append(slices.Clone(row.want), row.want...); fmt.Sprint(got) != fmt.Sprint(want) { + t.Errorf("%s: tested %v, want %v", row.name, got, want) + } + if a := out.(delivery.CheckAnswer); len(a.Levels) != 2 || len(a.Levels[0].Tests) != len(row.want) { + t.Errorf("%s: levels %+v, want both, each with every test result", row.name, a.Levels) + } + } + + r := &recorder{fetch: map[string]any{"workspace": "/w", "modules": []string{"a", "b"}, "changed": []string{"b/test/x.test.ts"}}, + tests: map[string]bool{"a": true, "b": false}} + s := r.steps() + s.Digest = nil + out, err := RunAsk(context.Background(), s, ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit, delivery.LevelIntegration}, + Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})) + if err != nil { + t.Fatal(err) + } + if a := out.(delivery.CheckAnswer); len(r.calls) != 5 || len(a.Levels) != 2 || len(a.Levels[0].Tests) != 2 { + t.Fatalf("calls %v, levels %+v: want b then a at each of the two levels", r.calls, a.Levels) + } +} diff --git a/modules/mesh-build-go/buildstep/blob.go b/modules/mesh-build-go/buildstep/blob.go new file mode 100644 index 00000000..127c6ace --- /dev/null +++ b/modules/mesh-build-go/buildstep/blob.go @@ -0,0 +1,170 @@ +package buildstep + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "syscall" + "time" +) + +// RegistryWait is how long a push waits for an artifact store that refuses connections, longer than its nightly +// collection holds it still (about a minute and a half, issue 457); RegistryFirstPause is the first pause, doubling +// to RegistryMostPause. Variables so a test shortens them. +var ( + RegistryWait = 5 * time.Minute + RegistryFirstPause = time.Second + RegistryMostPause = 10 * time.Second +) + +// BlobReference is where a pushed bundle or archive is, as a done key names it and the controller delivers from +// (novox/hq ADR 0498 clause 1): artifact-store:////blobs/, the store's address left out +// (issue 102). +func BlobReference(module, artifact, digest string) string { + return "artifact-store://" + module + "/" + artifact + "/blobs/" + digest +} + +// PushBlob puts body into the artifact store at registry (host:port) as a blob of repository (/) by +// its digest (`sha256:` and hex), from the build module's own process, after the build step ended (novox/hq ADR 0498 +// clauses 1 and 2): a blob the store already holds is done without an upload; otherwise it is uploaded (POST +// blobs/uploads/, then PUT with the digest). A store refusing connections is waited out up to RegistryWait; any other +// refusal fails, naming the store's status and its own words, for the caller's `not stored:` error. Bytes that are not +// their digest are refused before anything is sent. +func PushBlob(ctx context.Context, client *http.Client, registry, repository, digest string, body []byte) error { + sum := sha256.Sum256(body) + if got := "sha256:" + hex.EncodeToString(sum[:]); got != digest { + return fmt.Errorf("the blob is %s, not the digest %s it was to be pushed as", got, digest) + } + if client == nil { + client = http.DefaultClient + } + base := "http://" + registry + "/v2/" + repository + "/blobs/" + var present bool + if err := waitForStore(ctx, registry, func() error { + var err error + present, err = blobPresent(ctx, client, base+digest) + return err + }); err != nil { + return err + } + if present { + return nil + } + var location string + if err := waitForStore(ctx, registry, func() error { + var err error + location, err = startUpload(ctx, client, base+"uploads/") + return err + }); err != nil { + return err + } + target, err := url.Parse(base) + if err != nil { + return err + } + loc, err := target.Parse(location) + if err != nil { + return fmt.Errorf("the store answered the upload's location %q, which is not a URL: %w", location, err) + } + q := loc.Query() + q.Set("digest", digest) + loc.RawQuery = q.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodPut, loc.String(), bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/octet-stream") + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("putting %s of %s: %w", digest, repository, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + return storeSaid(resp, "putting "+digest+" of "+repository) + } + return nil +} + +func blobPresent(ctx context.Context, client *http.Client, at string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodHead, at, nil) + if err != nil { + return false, err + } + resp, err := client.Do(req) + if err != nil { + return false, err + } + resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusNotFound: + return false, nil + } + return false, fmt.Errorf("asking the store for %s: it answered %s", at, resp.Status) +} + +func startUpload(ctx context.Context, client *http.Client, at string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, at, nil) + if err != nil { + return "", err + } + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusAccepted { + return "", storeSaid(resp, "starting an upload at "+at) + } + location := resp.Header.Get("Location") + if location == "" { + return "", fmt.Errorf("starting an upload at %s: the store named no location", at) + } + return location, nil +} + +// storeSaid is the store's refusal: its status and the first of its own words. +func storeSaid(resp *http.Response, what string) error { + words, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return fmt.Errorf("%s: the store answered %s: %s", what, resp.Status, strings.TrimSpace(string(words))) +} + +// refused is whether an error is a connection refused. +func refused(err error) bool { + return err != nil && (errors.Is(err, syscall.ECONNREFUSED) || strings.Contains(err.Error(), "connection refused")) +} + +// waitForStore runs try, and while it fails because the store refuses connections, tries again with a growing pause +// until RegistryWait has passed. A refusal is the one error waited for: nothing was sent, so trying again does nothing +// twice. +func waitForStore(ctx context.Context, registry string, try func() error) error { + err := try() + if !refused(err) { + return err + } + started, pause := time.Now(), RegistryFirstPause + for { + left := RegistryWait - time.Since(started) + if left <= 0 { + return fmt.Errorf("the artifact store at %s refused every connection for %s, longer than its nightly collection holds it still, so it is down, not paused: %w", + registry, time.Since(started).Round(time.Millisecond), err) + } + select { + case <-ctx.Done(): + return fmt.Errorf("stopped while waiting for the artifact store at %s: %w (last: %v)", registry, ctx.Err(), err) + case <-time.After(min(pause, left)): + } + pause = min(pause*2, RegistryMostPause) + if err = try(); !refused(err) { + return err + } + } +} diff --git a/modules/mesh-build-go/buildstep/blob_test.go b/modules/mesh-build-go/buildstep/blob_test.go new file mode 100644 index 00000000..b77afb0c --- /dev/null +++ b/modules/mesh-build-go/buildstep/blob_test.go @@ -0,0 +1,151 @@ +package buildstep + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "io" + "net" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +func digestOfBytes(b []byte) string { + h := sha256.Sum256(b) + return "sha256:" + hex.EncodeToString(h[:]) +} + +// registry is an artifact store as far as a blob push needs one: HEAD a blob, start an upload, PUT it with its digest. +type registry struct { + mu sync.Mutex + blobs map[string][]byte + calls []string + put int // the status a PUT answers, 201 when 0 +} + +func (r *registry) ServeHTTP(w http.ResponseWriter, req *http.Request) { + r.mu.Lock() + defer r.mu.Unlock() + r.calls = append(r.calls, req.Method+" "+req.URL.Path) + switch { + case req.Method == http.MethodHead && strings.Contains(req.URL.Path, "/blobs/sha256:"): + if _, ok := r.blobs[req.URL.Path[strings.LastIndex(req.URL.Path, "/")+1:]]; ok { + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusNotFound) + case req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/blobs/uploads/"): + w.Header().Set("Location", strings.TrimSuffix(req.URL.Path, "/")+"/u1?state=x") + w.WriteHeader(http.StatusAccepted) + case req.Method == http.MethodPut && strings.Contains(req.URL.Path, "/blobs/uploads/u1"): + if r.put != 0 { + w.WriteHeader(r.put) + _, _ = io.WriteString(w, `{"errors":[{"code":"DIGEST_INVALID","message":"provided digest did not match uploaded content"}]}`) + return + } + body, _ := io.ReadAll(req.Body) + d := req.URL.Query().Get("digest") + if req.URL.Query().Get("state") != "x" || d != digestOfBytes(body) { + w.WriteHeader(http.StatusBadRequest) + return + } + r.blobs[d] = body + w.WriteHeader(http.StatusCreated) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// A done build's bundle or archive is pushed to the artifact store by its build module (novox/hq ADR 0498 clauses 1 +// and 2): a blob the store already holds is done with no upload; one it lacks is uploaded by digest; its reference is +// the artifact-store form the builder's key and the controller check. +func TestABlobIsPushedOnceByItsDigest(t *testing.T) { + reg := ®istry{blobs: map[string][]byte{}} + srv := httptest.NewServer(reg) + defer srv.Close() + addr := strings.TrimPrefix(srv.URL, "http://") + body := []byte("the packed bundle") + d := digestOfBytes(body) + if err := PushBlob(context.Background(), srv.Client(), addr, "mail/code", d, body); err != nil { + t.Fatal(err) + } + if string(reg.blobs[d]) != string(body) { + t.Fatalf("the store holds %q", reg.blobs[d]) + } + reg.calls = nil + if err := PushBlob(context.Background(), srv.Client(), addr, "mail/code", d, body); err != nil { + t.Fatal(err) + } + if len(reg.calls) != 1 || !strings.HasPrefix(reg.calls[0], "HEAD ") { + t.Fatalf("a blob the store holds was uploaded again: %v", reg.calls) + } + ref := BlobReference("mail", "code", d) + if ref != "artifact-store://mail/code/blobs/"+d { + t.Fatalf("BlobReference = %q", ref) + } + b := delivery.BuilderBuild{State: delivery.BuildDone, Manifest: strings.Repeat("4", 64), + Artifacts: []delivery.BuiltArtifact{{Name: "code", Kind: "bundle", Digest: d, Reference: ref}}} + if _, err := delivery.EncodeBuilderBuild(b); err != nil { + t.Fatalf("the key refuses the reference BlobReference gives: %v", err) + } +} + +// A store that refuses the upload fails the push naming the store's own words; bytes that are not the digest are +// refused before anything is sent. +func TestAPushTheStoreRefusesSaysTheStoresWords(t *testing.T) { + reg := ®istry{blobs: map[string][]byte{}, put: http.StatusBadRequest} + srv := httptest.NewServer(reg) + defer srv.Close() + addr := strings.TrimPrefix(srv.URL, "http://") + body := []byte("x") + err := PushBlob(context.Background(), srv.Client(), addr, "mail/code", digestOfBytes(body), body) + if err == nil || !strings.Contains(err.Error(), "provided digest did not match") || !strings.Contains(err.Error(), "400") { + t.Fatalf("a refused upload said %v", err) + } + reg.calls = nil + err = PushBlob(context.Background(), srv.Client(), addr, "mail/code", digestOfBytes([]byte("other")), body) + if err == nil || len(reg.calls) != 0 { + t.Fatalf("bytes that are not their digest: %v, calls %v", err, reg.calls) + } +} + +// A store refusing connections (its nightly collection holds it still) is waited out, up to RegistryWait (issue 457). +func TestAPushWaitsOutAStoreRefusingConnections(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + addr := l.Addr().String() + l.Close() // refusing now + reg := ®istry{blobs: map[string][]byte{}} + go func() { + time.Sleep(300 * time.Millisecond) + l2, err := net.Listen("tcp", addr) + if err != nil { + return + } + _ = http.Serve(l2, reg) + }() + beforeWait, beforePause := RegistryWait, RegistryFirstPause + RegistryWait, RegistryFirstPause = 3*time.Second, 100*time.Millisecond + defer func() { RegistryWait, RegistryFirstPause = beforeWait, beforePause }() + body := []byte("late") + if err := PushBlob(context.Background(), http.DefaultClient, addr, "mail/code", digestOfBytes(body), body); err != nil { + t.Fatalf("a store that came back was not waited for: %v", err) + } + + l3, _ := net.Listen("tcp", "127.0.0.1:0") + gone := l3.Addr().String() + l3.Close() + RegistryWait = 300 * time.Millisecond + err = PushBlob(context.Background(), http.DefaultClient, gone, "mail/code", digestOfBytes(body), body) + if err == nil || !strings.Contains(err.Error(), "refused every connection") { + t.Fatalf("a store that stayed down said %v", err) + } +} diff --git a/modules/mesh-build-go/buildstep/buildcpu/buildcpu.go b/modules/mesh-build-go/buildstep/buildcpu/buildcpu.go new file mode 100644 index 00000000..dcd2f364 --- /dev/null +++ b/modules/mesh-build-go/buildstep/buildcpu/buildcpu.go @@ -0,0 +1,215 @@ +// Package buildcpu is how much of a build node's processors its builds may take, and how they yield to everything else +// there (novox/hq ADR 0553, issue 1806; the operator, 2026-10-26: "limit CPU usage on our build nodes so the total cpu +// usage never impacts the normal workings of our nodes ... We have 3 build agents so they should share the cpu power"). +// +// Three parts, each with one owner: +// +// - **Priority.** A build seat's holder (catalogue.Seat.Yields) runs at the lowest priority there is: the composer +// gives its runtime process declaration.Yields, and the node-engine places every yielding process of the node in +// one slice (declaration.YieldSlice) whose CPU weight is idle, with Nice=19 for every process it starts. +// Interactive and service work always comes first; builds take the cycles nothing else wants. +// - **One cap per node, shared.** The node's share of its processors is one value, the node-engine's own config +// `build-cpu-share` (ShareSetting), set per node through the node's settings layer. The node-engine writes it as the +// slice's CPUQuota: one bound the kernel holds for all the node's build modules together, never one per module. +// - **Threads.** The quota bounds the total; threads only size each step. A step gets the share divided by its own +// module's capacity (Threads), from the share the composer hands as a word (ShareWord): GOMAXPROCS sets every go +// command's -p, the compiler's concurrency, a test binary's processors and -parallel. A module given no word sets +// no thread limit at all: without the slice there is nothing to size a step to, and a limit then would only slow +// builds. +package buildcpu + +import ( + "os" + "runtime" + "strconv" + "strings" +) + +// ShareSetting is the node-engine's config value of the node's processor share for its builds, in percent of the +// machine's processors, 1 to 100: one value per node, set through the node's settings layer (novox/hq ADR 0553 §3). +const ShareSetting = "build-cpu-share" + +// DefaultPercent is the share where nothing sets one: half the machine, so a desktop keeps half its processors for +// itself whatever builds. +const DefaultPercent = 50 + +// ShareWord is the word the composer gives every yielding module (novox/hq ADR 0553 §4): the node's share, in percent. +const ShareWord = "MESH_BUILD_CPU_SHARE" + +// Quota is a CPUQuota for a share of a machine with cores processors: systemd counts a quota in percent of one +// processor, so half of 24 is 1200%. At least 100%, one processor, so a share on a small machine never stops a step. +func Quota(percent, cores int) int { + q := percent * cores + if q < 100 { + q = 100 + } + return q +} + +// Threads is how many threads one step may run at once: its part of the share when its own module runs slots steps at +// once, at least one. The slice's quota bounds the steps of all the node's build modules together; this only sizes one. +func Threads(percent, cores, slots int) int { + if slots < 1 { + slots = 1 + } + if percent < 1 || percent > 100 { + percent = DefaultPercent + } + t := cores * percent / 100 / slots + if t < 1 { + t = 1 + } + return t +} + +// Limits is what one step is held to on this node, as a status tool says it. +type Limits struct { + // Percent is the node's share in percent of its processors, and From where it was read: ShareWord, or "none" when + // the composer gave no share and a step runs without a thread limit. + Percent int `json:"share-percent,omitempty"` + From string `json:"share-from"` + // Cores is the machine's processor count. + Cores int `json:"cores"` + // Slots is this module's own capacity on the node: steps at once. + Slots int `json:"slots"` + // Threads is what each step is given (Threads); 0 is no limit. + Threads int `json:"threads-per-step"` +} + +// FromWords is the limits of a step from the share the composer gave this process (ShareWord) and the module's own +// capacity. Without a usable share there is no limit: Threads is 0. +func FromWords(getenv func(string) string, own int) Limits { + if own < 1 { + own = 1 + } + l := Limits{From: "none", Cores: runtime.NumCPU(), Slots: own} + if n, err := strconv.Atoi(strings.TrimSpace(getenv(ShareWord))); err == nil && n >= 1 && n <= 100 { + l.Percent, l.From = n, ShareWord + l.Threads = Threads(n, l.Cores, own) + } + return l +} + +// Env is the environment a step's commands are given so they start no more threads than they may: GOMAXPROCS for every +// Go program (the compiler, a test binary, esbuild, buildah), UV_THREADPOOL_SIZE for Node's worker pool. Returned as +// KEY=value words to append to a command's environment; nothing for 0 threads, no limit. +func Env(threads int) []string { + if threads < 1 { + return nil + } + t := strconv.Itoa(threads) + return []string{"GOMAXPROCS=" + t, "UV_THREADPOOL_SIZE=" + t} +} + +// InForce is what the kernel holds this process to, read rather than assumed (evidence over assertion): its nice +// level, its cgroup (the unit) and the cgroup's parent (the slice the node-engine placed it in), each one's CPU weight, +// idle mark and quota. A field the machine does not answer is left empty and Unread says which. +// +// The IO weight is read but binds only where the disk's scheduler honours it (bfq, or the io.cost controller); under +// the scheduler `none`, the usual one for NVMe, it does nothing, and the CPU bound is what holds. +type InForce struct { + Nice int `json:"nice"` + Cgroup string `json:"cgroup,omitempty"` + Unit Group `json:"unit"` + Slice Group `json:"slice"` + // Quota is the tighter of the two groups' cpu.max as a percent of one processor, 0 with none. + Quota int `json:"quota-percent,omitempty"` + Unread []string `json:"unread,omitempty"` +} + +// Group is one cgroup's controls. +type Group struct { + Path string `json:"path,omitempty"` + CPUIdle string `json:"cpu-idle,omitempty"` + CPUWeight string `json:"cpu-weight,omitempty"` + // CPUMax is the cgroup's cpu.max: " ", or "max" with no quota. + CPUMax string `json:"cpu-max,omitempty"` + IOWeight string `json:"io-weight,omitempty"` +} + +// cgroupRoot is where the unified hierarchy is mounted. A variable so a test can point it elsewhere. +var cgroupRoot = "/sys/fs/cgroup" + +// procSelfCgroup is this process's cgroup file. A variable so a test can point it elsewhere. +var procSelfCgroup = "/proc/self/cgroup" + +// procSelfStat is this process's stat file. A variable so a test can point it elsewhere. +var procSelfStat = "/proc/self/stat" + +// ReadInForce reads what the kernel holds this process to. +func ReadInForce() InForce { + var f InForce + f.Nice = nice() + raw, err := os.ReadFile(procSelfCgroup) + if err != nil { + f.Unread = append(f.Unread, "cgroup: "+err.Error()) + return f + } + for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { + if strings.HasPrefix(line, "0::") { + f.Cgroup = strings.TrimPrefix(line, "0::") + } + } + if f.Cgroup == "" { + f.Unread = append(f.Unread, "cgroup: not on the unified hierarchy") + return f + } + read := func(path string) Group { + g := Group{Path: path} + value := func(name string) string { + b, err := os.ReadFile(cgroupRoot + path + "/" + name) + if err != nil { + f.Unread = append(f.Unread, path+"/"+name+": "+err.Error()) + return "" + } + return strings.TrimSpace(string(b)) + } + g.CPUIdle, g.CPUWeight, g.CPUMax, g.IOWeight = value("cpu.idle"), value("cpu.weight"), value("cpu.max"), value("io.weight") + return g + } + f.Unit = read(f.Cgroup) + if i := strings.LastIndex(f.Cgroup, "/"); i > 0 { + f.Slice = read(f.Cgroup[:i]) + } + for _, q := range []int{quotaOf(f.Unit.CPUMax), quotaOf(f.Slice.CPUMax)} { + if q > 0 && (f.Quota == 0 || q < f.Quota) { + f.Quota = q + } + } + return f +} + +// quotaOf is a cpu.max value as a percent of one processor: "1200000 100000" is 1200; "max ..." or nothing is 0. +func quotaOf(cpuMax string) int { + fields := strings.Fields(cpuMax) + if len(fields) != 2 || fields[0] == "max" { + return 0 + } + q, err1 := strconv.Atoi(fields[0]) + p, err2 := strconv.Atoi(fields[1]) + if err1 != nil || err2 != nil || p <= 0 { + return 0 + } + return q * 100 / p +} + +// nice is this process's nice level from its stat file (the 19th field, counted after the command's closing +// parenthesis, which may itself hold spaces); 0 when it cannot be read. +func nice() int { + raw, err := os.ReadFile(procSelfStat) + if err != nil { + return 0 + } + s := string(raw) + i := strings.LastIndex(s, ")") + if i < 0 { + return 0 + } + fields := strings.Fields(s[i+1:]) + // After the command: state is field 3, so nice (field 19) is index 16. + if len(fields) < 17 { + return 0 + } + n, _ := strconv.Atoi(fields[16]) + return n +} diff --git a/modules/mesh-build-go/buildstep/buildcpu/buildcpu_test.go b/modules/mesh-build-go/buildstep/buildcpu/buildcpu_test.go new file mode 100644 index 00000000..67406870 --- /dev/null +++ b/modules/mesh-build-go/buildstep/buildcpu/buildcpu_test.go @@ -0,0 +1,90 @@ +package buildcpu + +import ( + "os" + "path/filepath" + "reflect" + "runtime" + "testing" +) + +// The node's share as a quota, the threads one step is given for its own module's capacity, and the word the composer +// hands, in one table (ADR 0553, issue 1806). +func TestShareQuotaAndThreads(t *testing.T) { + for _, c := range []struct { + name string + percent, cores, slots int + quota, threads int + }{ + {"shanks: half of 24, three slots", 50, 24, 3, 1200, 4}, + {"ace: three quarters of 24, two slots", 75, 24, 2, 1800, 9}, + {"g14: half of 16, one slot", 50, 16, 1, 800, 8}, + {"one processor at least", 10, 2, 4, 100, 1}, + {"no slots counts as one", 100, 4, 0, 400, 4}, + {"a share out of range is the default", 0, 8, 1, 0, 4}, + } { + if c.quota != 0 { + if q := Quota(c.percent, c.cores); q != c.quota { + t.Errorf("%s: Quota = %d, want %d", c.name, q, c.quota) + } + } + if th := Threads(c.percent, c.cores, c.slots); th != c.threads { + t.Errorf("%s: Threads = %d, want %d", c.name, th, c.threads) + } + } + l := FromWords(func(k string) string { return map[string]string{ShareWord: "75"}[k] }, 2) + if l.Percent != 75 || l.From != ShareWord || l.Slots != 2 || l.Threads != Threads(75, runtime.NumCPU(), 2) { + t.Errorf("from the word: %+v", l) + } + // Told nothing usable, a step has no thread limit: nothing in its environment. + for _, word := range []string{"", "250", "lots"} { + l := FromWords(func(string) string { return word }, 3) + if l.Threads != 0 || l.From != "none" || Env(l.Threads) != nil { + t.Errorf("share %q: %+v, env %v", word, l, Env(l.Threads)) + } + } + if got := Env(3); !reflect.DeepEqual(got, []string{"GOMAXPROCS=3", "UV_THREADPOOL_SIZE=3"}) { + t.Errorf("Env = %q", got) + } +} + +// What the kernel holds a process to is read from its unit's and its slice's cgroup and its stat file, not assumed. +func TestReadInForce(t *testing.T) { + dir := t.TempDir() + cg := filepath.Join(dir, "cg") + slice := filepath.Join(cg, "meshyield.slice") + unit := filepath.Join(slice, "mesh-build-go-runtime.service") + if err := os.MkdirAll(unit, 0o755); err != nil { + t.Fatal(err) + } + for at, files := range map[string]map[string]string{ + slice: {"cpu.idle": "1\n", "cpu.weight": "0\n", "cpu.max": "1200000 100000\n", "io.weight": "default 1\n"}, + unit: {"cpu.idle": "0\n", "cpu.weight": "100\n", "cpu.max": "max 100000\n", "io.weight": "default 100\n"}, + } { + for name, v := range files { + if err := os.WriteFile(filepath.Join(at, name), []byte(v), 0o644); err != nil { + t.Fatal(err) + } + } + } + self := filepath.Join(dir, "cgroup") + stat := filepath.Join(dir, "stat") + _ = os.WriteFile(self, []byte("0::/meshyield.slice/mesh-build-go-runtime.service\n"), 0o644) + // A command with a space and a parenthesis in it, then nice 19 in field 19. + _ = os.WriteFile(stat, []byte("42 (mesh (build) go) S 1 42 42 0 -1 4194560 1 0 0 0 0 0 0 0 39 19 8 0 1 2 3\n"), 0o644) + cgroupRoot, procSelfCgroup, procSelfStat = cg, self, stat + t.Cleanup(func() { + cgroupRoot, procSelfCgroup, procSelfStat = "/sys/fs/cgroup", "/proc/self/cgroup", "/proc/self/stat" + }) + + got := ReadInForce() + want := InForce{Nice: 19, Cgroup: "/meshyield.slice/mesh-build-go-runtime.service", + Unit: Group{Path: "/meshyield.slice/mesh-build-go-runtime.service", CPUIdle: "0", CPUWeight: "100", + CPUMax: "max 100000", IOWeight: "default 100"}, + Slice: Group{Path: "/meshyield.slice", CPUIdle: "1", CPUWeight: "0", CPUMax: "1200000 100000", + IOWeight: "default 1"}, + Quota: 1200} + if !reflect.DeepEqual(got, want) { + t.Fatalf("ReadInForce = %+v\nwant %+v", got, want) + } +} diff --git a/modules/mesh-build-go/buildstep/buildhours.go b/modules/mesh-build-go/buildstep/buildhours.go new file mode 100644 index 00000000..6e3dee47 --- /dev/null +++ b/modules/mesh-build-go/buildstep/buildhours.go @@ -0,0 +1,129 @@ +package buildstep + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" +) + +// **A node may build only within its build hours** (the operator, 2026-10-26 21:18: the laptop builds only outside +// office hours, "from 18 o'clock until 6 in the morning"). A build module's `build-hours` setting is the node's window +// in its local time, a config value set on each node's layer of mesh-config (ADR 0567 §12: no repository names a node); +// outside it the capacity judged by is 0: steps running finish, no new one is taken. An empty value builds at any hour. +// The puller judges it at every judgement and wakes on one timer to the window's next edge (a clock edge, not a poll of +// another part). + +// BuildHoursSetting is the build modules' setting of the node's build window: HH:MM-HH:MM in the node's local time; an +// end before its start crosses midnight (18:00-06:00). +const BuildHoursSetting = "build-hours" + +// BuildWindow is one node's build hours: minutes after local midnight, Start inclusive, End exclusive. +type BuildWindow struct { + Start, End int +} + +// String is the window as the setting writes it. +func (w BuildWindow) String() string { + return fmt.Sprintf("%02d:%02d-%02d:%02d", w.Start/60, w.Start%60, w.End/60, w.End%60) +} + +// Open says whether t, in its own location, falls in the window. +func (w BuildWindow) Open(t time.Time) bool { + m := t.Hour()*60 + t.Minute() + if w.Start < w.End { + return m >= w.Start && m < w.End + } + return m >= w.Start || m < w.End +} + +// NextEdge is the first time after t, in t's location, at which the window opens or closes. +func (w BuildWindow) NextEdge(t time.Time) time.Time { + var next time.Time + for day := 0; day <= 1; day++ { + for _, m := range []int{w.Start, w.End} { + e := time.Date(t.Year(), t.Month(), t.Day()+day, m/60, m%60, 0, 0, t.Location()) + if e.After(t) && (next.IsZero() || e.Before(next)) { + next = e + } + } + } + return next +} + +// ParseBuildWindow reads a build-hours value, HH:MM-HH:MM: nil when it is empty. +func ParseBuildWindow(setting string) (*BuildWindow, error) { + setting = strings.TrimSpace(setting) + if setting == "" { + return nil, nil + } + from, to, ok := strings.Cut(setting, "-") + if !ok { + return nil, fmt.Errorf("the build hours %q are not HH:MM-HH:MM", setting) + } + start, err := minuteOf(from) + if err != nil { + return nil, fmt.Errorf("the build hours %q: %v", setting, err) + } + end, err := minuteOf(to) + if err != nil { + return nil, fmt.Errorf("the build hours %q: %v", setting, err) + } + if start == end { + return nil, fmt.Errorf("the build hours %q open and close at the same minute", setting) + } + return &BuildWindow{Start: start, End: end}, nil +} + +// minuteOf reads HH:MM as minutes after midnight. +func minuteOf(s string) (int, error) { + var h, m int + s = strings.TrimSpace(s) + if n, err := fmt.Sscanf(s, "%d:%d", &h, &m); err != nil || n != 2 || h < 0 || h > 23 || m < 0 || m > 59 || len(s) != 5 { + return 0, fmt.Errorf("%q is not a time HH:MM", s) + } + return h*60 + m, nil +} + +// NodeBuildHours is the node's build window, read from the module's settings file at path at every call, and why it is +// none when the value cannot be read. No path, no file value or an empty one is no window and no fault. A value that +// cannot be read is no window too, so the node builds at any hour; the setting is a preference of hours, not a safety +// bound (a node that must never build has capacity 0). It is loud: the fault is said once per distinct fault through +// logf, and the puller writes it in the capacity record that build_usage shows. +func NodeBuildHours(path, node string, logf func(string, ...any)) func() (*BuildWindow, string) { + said := "" + say := func(why string) { + if logf != nil && why != said { + said = why + logf("%s, so %s builds at any hour", why, node) + } + } + fault := func(why string) (*BuildWindow, string) { + say(why) + return nil, why + } + return func() (*BuildWindow, string) { + if path == "" { + return nil, "" + } + raw, err := os.ReadFile(path) + if err != nil { + return fault(fmt.Sprintf("the setting %s cannot be read: %v", BuildHoursSetting, err)) + } + var s map[string]json.RawMessage + if err := json.Unmarshal(raw, &s); err != nil { + return fault(fmt.Sprintf("the settings file %s is not JSON: %v", path, err)) + } + setting := "" + if v, ok := s[BuildHoursSetting]; ok && json.Unmarshal(v, &setting) != nil { + return fault(fmt.Sprintf("the setting %s %s is not a string", BuildHoursSetting, v)) + } + w, err := ParseBuildWindow(setting) + if err != nil { + return fault(err.Error()) + } + said = "" + return w, "" + } +} diff --git a/modules/mesh-build-go/buildstep/capacity.go b/modules/mesh-build-go/buildstep/capacity.go new file mode 100644 index 00000000..d545fb30 --- /dev/null +++ b/modules/mesh-build-go/buildstep/capacity.go @@ -0,0 +1,57 @@ +package buildstep + +import ( + sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep" + + "encoding/json" + "fmt" + "os" +) + +// NodeCapacity is the node's build capacity, read from the module's settings file at path at every call, so a change +// takes effect at the next pull. No path, no file value, or a value that is not a whole number of at least 0 gives 0 and +// a value that cannot be read says why: pulling on a setting nobody can read could build on a node that must not. +func NodeCapacity(path, node string, logf func(string, ...any)) func() int { + say := func(format string, args ...any) { + if logf != nil { + logf(format, args...) + } + } + return func() int { + n, err := ReadCapacity(path) + if err != nil { + say("%v, so %s pulls nothing", err, node) + return 0 + } + return n + } +} + +// ReadCapacity is the capacity the settings file at path holds, and why it cannot be read when it cannot (then 0). +func ReadCapacity(path string) (int, error) { + if path == "" { + return 0, nil + } + raw, err := os.ReadFile(path) + if err != nil { + return 0, fmt.Errorf("the build capacity cannot be read: %w", err) + } + var s map[string]json.RawMessage + if err := json.Unmarshal(raw, &s); err != nil { + return 0, fmt.Errorf("the settings file %s is not JSON: %w", path, err) + } + return capacityOf(s[sdkstep.CapacitySetting]) +} + +// capacityOf is the capacity a config value holds: absent or null is 0; anything but a whole number of at least 0 is +// refused, naming it. +func capacityOf(value json.RawMessage) (int, error) { + if len(value) == 0 || string(value) == "null" { + return 0, nil + } + var n int + if err := json.Unmarshal(value, &n); err != nil || n < 0 { + return 0, fmt.Errorf("the build capacity %s is not a whole number of asks at once, 0 or more", value) + } + return n, nil +} diff --git a/modules/mesh-build-go/buildstep/capacity_cpu.go b/modules/mesh-build-go/buildstep/capacity_cpu.go new file mode 100644 index 00000000..f8f12540 --- /dev/null +++ b/modules/mesh-build-go/buildstep/capacity_cpu.go @@ -0,0 +1,141 @@ +package buildstep + +import ( + "encoding/json" + "fmt" + "math" + "os" + "runtime" + "strconv" + "strings" + + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/buildcpu" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// **A build seat's capacity follows the CPU its node gives builds** (issue 1806's follow-up; issue 1890, 2026-10-26 +// 19:41: under the 50 % cap the workstation's mesh-build-go kept capacity 3, three full mesh-catalog checks shared 12 cores, and +// each step ran past the 30-minute step bound with no test run). The configured capacity is an upper bound; the node's +// build slice bounds it again by the cores a step needs: floor(the slice's quota in cores / cores-per-step), one at +// least. The quota is read live from the cgroup at every judgement, so a changed cap is followed at the next take +// without a restart, and the result is said (the seat's current verb, the status tools, `capacity.` in the pauses +// state that the delivery view's build_usage reads). + +// CoresPerStepSetting is a build module's setting of the processors one of its steps needs: a number above zero, the +// same on every node (a step's need is the module's, not the node's). Each build module declares it with its own default +// (DefaultCoresPerStep). +const CoresPerStepSetting = "cores-per-step" + +// DefaultCoresPerStep are the build modules' defaults by language, from measured steps (2026-10-26): a full mesh-catalog +// check on mesh-build-go takes what it is given (three at once took 965 % of the workstation's 1200 % slice, about 4 cores each, +// and ran past the 30-minute step bound), so a Go step is given 6, half again what starved; a Node step (tsc, esbuild, a +// test runner: median 15 s) and an image build (buildah, its RUN lines mostly fetches) need 2. +var DefaultCoresPerStep = map[string]float64{delivery.AskGo: 6, delivery.AskNode: 2, delivery.AskImage: 2} + +// Quota is the build slice's CPU quota as this process's cgroup holds it, in percent of one processor (1200 is 12 +// cores), and where it was read: the cgroup's cpu.max (buildcpu.ReadInForce, the tighter of its unit's and its slice's), +// else the share the composer gave (buildcpu.ShareWord) over the machine's processors, else 0 and why none is known. +func Quota() (percent int, from string) { + f := buildcpu.ReadInForce() + if f.Quota > 0 { + path := f.Slice.Path + if path == "" { + path = f.Cgroup + } + return f.Quota, "the cpu.max of " + path + } + if n, err := strconv.Atoi(strings.TrimSpace(os.Getenv(buildcpu.ShareWord))); err == nil && n >= 1 && n <= 100 { + return buildcpu.Quota(n, runtime.NumCPU()), fmt.Sprintf("the share %d%% the composer gave (%s), of %d processors; no cpu.max is read", n, buildcpu.ShareWord, runtime.NumCPU()) + } + return 0, "no CPU quota: neither a cpu.max on its cgroup nor a share from the composer" +} + +// EffectiveCapacity is the capacity a seat's holder judges by, from its configured capacity, its node's quota (percent of +// one processor, 0 when none is known) and the cores one of its steps needs. Without a quota it is the configured one; +// with one, min(configured, floor(quota cores / coresPerStep)), and one at least while the configured one is above +// zero. A coresPerStep of zero or less is taken as 1. +func EffectiveCapacity(configured, quotaPercent int, quotaFrom string, coresPerStep float64) delivery.Capacity { + if coresPerStep <= 0 || math.IsNaN(coresPerStep) || math.IsInf(coresPerStep, 0) { + coresPerStep = 1 + } + c := delivery.Capacity{Configured: configured, Effective: configured, QuotaFrom: quotaFrom, CoresPerStep: coresPerStep} + switch { + case configured <= 0: + c.Effective = 0 + c.Why = fmt.Sprintf("its configured capacity is %d, so it pulls nothing", configured) + return c + case quotaPercent <= 0: + c.Why = fmt.Sprintf("its configured capacity %d; %s, so the CPU bounds nothing", configured, quotaFrom) + return c + } + c.QuotaCores = float64(quotaPercent) / 100 + c.ByCPU = int(math.Floor(c.QuotaCores/coresPerStep + 1e-9)) + if c.ByCPU < 1 { + c.ByCPU = 1 + } + byCPU := fmt.Sprintf("%g cores of the build slice (%s) / %g cores a step = %d", c.QuotaCores, quotaFrom, coresPerStep, c.ByCPU) + if c.ByCPU < configured { + c.Effective = c.ByCPU + c.Why = fmt.Sprintf("%d, bounded by the CPU: %s, under its configured %d", c.Effective, byCPU, configured) + } else { + c.Why = fmt.Sprintf("%d, its configured capacity; the CPU allows %s", configured, byCPU) + } + return c +} + +// NodeCoresPerStep is the module's cores-per-step, read from its settings file at path at every call so a change is +// followed at the next judgement; fallback when there is no path, no value or one that is not a number above zero, said +// once per distinct fault through logf. +func NodeCoresPerStep(path string, fallback float64, logf func(string, ...any)) func() float64 { + said := "" + say := func(why string) { + if logf != nil && why != said { + said = why + logf("%s, so a step is taken to need %g cores", why, fallback) + } + } + return func() float64 { + if path == "" { + return fallback + } + raw, err := os.ReadFile(path) + if err != nil { + say(fmt.Sprintf("the setting %s cannot be read: %v", CoresPerStepSetting, err)) + return fallback + } + var s map[string]json.RawMessage + if err := json.Unmarshal(raw, &s); err != nil { + say(fmt.Sprintf("the settings file %s is not JSON: %v", path, err)) + return fallback + } + v, ok := s[CoresPerStepSetting] + if !ok { + return fallback // not declared yet (a module before its manifest names it): the default, nothing to say + } + n, err := coresOf(v) + if err != nil { + say(fmt.Sprintf("the setting %s %s: %v", CoresPerStepSetting, v, err)) + return fallback + } + said = "" + return n + } +} + +// coresOf reads a cores-per-step value: a JSON number or a string holding one, above zero. +func coresOf(v json.RawMessage) (float64, error) { + var n float64 + if err := json.Unmarshal(v, &n); err != nil { + var s string + if err := json.Unmarshal(v, &s); err != nil { + return 0, fmt.Errorf("is neither a number nor a string") + } + if n, err = strconv.ParseFloat(strings.TrimSpace(s), 64); err != nil { + return 0, fmt.Errorf("is not a number") + } + } + if n <= 0 || math.IsNaN(n) || math.IsInf(n, 0) { + return 0, fmt.Errorf("is not above zero") + } + return n, nil +} diff --git a/modules/mesh-build-go/buildstep/capacity_test.go b/modules/mesh-build-go/buildstep/capacity_test.go new file mode 100644 index 00000000..aef58c6e --- /dev/null +++ b/modules/mesh-build-go/buildstep/capacity_test.go @@ -0,0 +1,135 @@ +package buildstep + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// 1887/A7: the node's build capacity is the build module's setting, read at every pull: the control-node at 0 pulls +// nothing, a change takes effect at the next read, and a setting that cannot be read pulls nothing. +// ADR 0553 §4: a step's threads are the composer's share over the module's own capacity; without the word, none. And +// issue 1806's follow-up: the capacity judged by is the configured one bounded by the build slice's cores over the cores +// a step needs (the three build nodes as they stand on 2026-10-26), and cores-per-step is read from the settings file. +func TestNodeCapacityAndLimits(t *testing.T) { + // ADR 0567 §12: the capacity is the node's integer, its default 0, and no default names a node. + if got := NodeCapacity("", "laptop", nil)(); got != 0 { + t.Errorf("no settings file = %d, want 0", got) + } + file := filepath.Join(t.TempDir(), "settings.json") + capacity := NodeCapacity(file, "anchor", nil) + for value, want := range map[string]int{`{"capacity":1}`: 1, `{"capacity":4}`: 4, `{}`: 0, `{"capacity":null}`: 0, + `{"capacity":"anchor=3"}`: 0, `{"capacity":-1}`: 0, `{"capacity":1.5}`: 0} { + os.WriteFile(file, []byte(value), 0o600) + if got := capacity(); got != want { + t.Errorf("%s = %d, want %d", value, got, want) + } + } + if got := NodeCapacity(filepath.Join(t.TempDir(), "none"), "anchor", nil)(); got != 0 { + t.Errorf("a missing file = %d, want 0", got) + } + t.Setenv("MESH_BUILD_CPU_SHARE", "") + if l := NodeLimits(3); l.Threads != 0 || l.Slots != 3 { + t.Errorf("without the word: %+v", l) + } + t.Setenv("MESH_BUILD_CPU_SHARE", "75") + if l := NodeLimits(3); l.Percent != 75 || l.Slots != 3 || l.Threads < 1 { + t.Errorf("from the word: %+v", l) + } + for _, c := range []struct { + name string + configured, quota int + cores float64 + effective, byCPU int + }{ + {"shanks: 12 cores, configured 3", 3, 1200, 6, 2, 2}, + {"ace: 18 cores, configured 2", 2, 1800, 6, 2, 3}, + {"g14: 8 cores, configured 2", 2, 800, 6, 1, 1}, + {"node on shanks: 12 cores at 2", 3, 1200, 2, 3, 6}, + {"no quota: the configured one", 3, 0, 6, 3, 0}, + {"configured 0 pulls nothing", 0, 1200, 6, 0, 0}, + {"a quota under one step still builds one", 2, 100, 6, 1, 1}, + {"no cores a step is one", 2, 150, 0, 1, 1}, + } { + got := EffectiveCapacity(c.configured, c.quota, "test", c.cores) + if got.Effective != c.effective || got.ByCPU != c.byCPU || got.Why == "" { + t.Errorf("%s: %+v, want effective %d, by CPU %d", c.name, got, c.effective, c.byCPU) + } + } + file = filepath.Join(t.TempDir(), "settings.json") + var said []string + cores := NodeCoresPerStep(file, 6, func(f string, a ...any) { said = append(said, f) }) + for _, c := range []struct { + file string + want float64 + }{{`{"capacity":2}`, 6}, {`{"cores-per-step":4}`, 4}, {`{"cores-per-step":"3.5"}`, 3.5}, {`{"cores-per-step":0}`, 6}, {`{"cores-per-step":0}`, 6}} { + os.WriteFile(file, []byte(c.file), 0o600) + if got := cores(); got != c.want { + t.Errorf("cores-per-step from %s = %g, want %g", c.file, got, c.want) + } + } + if len(said) != 1 { + t.Errorf("a value not above zero is said once, said %d times", len(said)) + } + + // The operator, 2026-10-26 21:18: g14 builds only from 18:00 to 06:00, its local time. Outside its build hours the + // capacity judged by is 0, said with the window and when it opens; a node not named, or a bad value, builds at any hour. + // And a node its module proves cannot build (ace without /dev/net/tun, 2026-10-28) judges by 0 at every hour, saying + // why, so its asks go to a node that can. + cet := time.FixedZone("CET", 3600) + day := func(hh, mm int) time.Time { return time.Date(2026, 10, 26, hh, mm, 0, 0, cet) } + was := now + defer func() { now = was }() + for _, c := range []struct { + name, setting, node string + at time.Time + effective int + edge string // the window's next edge, HH:MM; "" for no window + bad bool + ineligible string // what the module's Eligible says; "" for eligible + }{ + {"the laptop in the office", "18:00-06:00", "laptop", day(10, 0), 0, "18:00", false, ""}, + {"the laptop in the evening", "18:00-06:00", "laptop", day(18, 0), 2, "06:00", false, ""}, + {"the laptop past midnight", "18:00-06:00", "laptop", day(5, 59), 2, "06:00", false, ""}, + {"the laptop at the close", "18:00-06:00", "laptop", day(6, 0), 0, "18:00", false, ""}, + {"a day window", "09:00-17:30", "anchor", day(17, 29), 2, "17:30", false, ""}, + {"no window", "", "anchor", day(10, 0), 2, "", false, ""}, + {"a bad time", "18:00-6", "laptop", day(10, 0), 2, "", true, ""}, + {"no span", "18:00-18:00", "laptop", day(10, 0), 2, "", true, ""}, + {"the old form naming a node", "laptop=18:00-06:00", "laptop", day(10, 0), 2, "", true, ""}, + {"a node without /dev/net/tun", "", "anchor", day(10, 0), 0, "", false, "/dev/net/tun cannot be opened: no such device"}, + {"not eligible within its hours", "18:00-06:00", "laptop", day(19, 0), 0, "06:00", false, "/dev/net/tun cannot be opened: no such device"}, + } { + w, err := ParseBuildWindow(c.setting) + if (err != nil) != c.bad { + t.Errorf("%s: parse %v", c.name, err) + } + if edge := ""; w != nil { + edge = w.NextEdge(c.at).Format("15:04") + if edge != c.edge { + t.Errorf("%s: next edge %s, want %s", c.name, edge, c.edge) + } + } else if c.edge != "" { + t.Errorf("%s: no window, want one", c.name) + } + file := filepath.Join(t.TempDir(), "settings.json") + os.WriteFile(file, []byte(`{"build-hours":"`+c.setting+`"}`), 0o600) + now = func() time.Time { return c.at } + p := &Puller{Language: "go", Node: c.node, Capacity: func() int { return 2 }, BuildHours: NodeBuildHours(file, c.node, nil), + Eligible: func() string { return c.ineligible }} + got := p.CapacityDetail() + outside := c.effective == 0 && c.ineligible == "" + if got.Effective != c.effective || got.OutsideHours != outside || (outside && !strings.Contains(got.Why, "outside its build hours 18:00-06:00 CET until 18:00")) { + t.Errorf("%s: %+v, want capacity %d", c.name, got, c.effective) + } + if got.Ineligible != c.ineligible || (c.ineligible != "" && !strings.HasPrefix(got.Why, "0, not eligible: "+c.ineligible+" (otherwise ")) { + t.Errorf("%s: not eligible is not said: %+v", c.name, got) + } + // A value that cannot be read builds at any hour, loudly: the record names the value and says why. + if c.bad && (!strings.Contains(got.BuildHoursFault, c.setting) || !strings.Contains(got.Why, "at any hour, because its build hours cannot be read")) { + t.Errorf("%s: a bad value is not said in the record: %+v", c.name, got) + } + } +} diff --git a/modules/mesh-build-go/buildstep/cmd/mesh-pack/main.go b/modules/mesh-build-go/buildstep/cmd/mesh-pack/main.go new file mode 100644 index 00000000..5b03c5dc --- /dev/null +++ b/modules/mesh-build-go/buildstep/cmd/mesh-pack/main.go @@ -0,0 +1,111 @@ +// mesh-pack builds a Go bundle by hand with the recipe of the mesh-build-go module and packs it with mesh-sdk's own +// buildstep.Pack, printing the digest, so a hand build has the form of a built artifact and, with the same Go version, +// its digest (novox/hq ADR 0526; fallback record F1; issue 1499 for the Go version). +// +// mesh-pack -dir -from -binary [-system arch] [-out ] +// mesh-pack -pack [-out ] (a `files` archive: pack a tree as it is) +// +// The recipe is mesh-catalog modules/mesh-build-go cmd/mesh-build-go/build.go (recipeFlags, recipeLinkerFlags, +// recipeSystemStamp, buildEnv): CGO_ENABLED=0, GOFLAGS "-trimpath -buildvcs=false -mod=mod" (-mod=vendor when the +// directory vendors), GOTOOLCHAIN=local, GOPROXY=off, and -ldflags "-s -w -X main.builtFor=". +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" +) + +const ( + recipeFlags = "-trimpath -buildvcs=false" + recipeLinkerFlags = "-s -w" + recipeSystemStamp = "main.builtFor" +) + +// Env is the build module's environment for the compile of dir, added to the caller's own. +func Env(dir string) []string { + mod := "-mod=mod" + if _, err := os.Stat(filepath.Join(dir, "vendor", "modules.txt")); err == nil { + mod = "-mod=vendor" + } + return append(os.Environ(), + "CGO_ENABLED=0", + "GOFLAGS="+recipeFlags+" "+mod, + "GOTOOLCHAIN=local", + "GOPROXY=off", + ) +} + +// Args is the arguments of the go command of the recipe. +func Args(dir, from, out, binary, system string) []string { + linker := recipeLinkerFlags + if s := strings.TrimSpace(system); s != "" { + linker += " -X " + recipeSystemStamp + "=" + s + } + return []string{"-C", dir, "build", "-ldflags", linker, "-o", filepath.Join(out, binary), "./" + strings.Trim(from, "./")} +} + +// Build compiles the package into a fresh directory and answers it. +func Build(dir, from, binary, system string) (string, error) { + out, err := os.MkdirTemp("", "mesh-pack-") + if err != nil { + return "", err + } + cmd := exec.Command("go", Args(dir, from, out, binary, system)...) + cmd.Env = Env(dir) + cmd.Stdout, cmd.Stderr = os.Stderr, os.Stderr + if err := cmd.Run(); err != nil { + os.RemoveAll(out) + return "", fmt.Errorf("go build of %s: %w", from, err) + } + return out, nil +} + +func run(args []string) (string, error) { + fs := flag.NewFlagSet("mesh-pack", flag.ContinueOnError) + dir := fs.String("dir", ".", "the Go module's directory") + from := fs.String("from", "", "the package to build, relative to -dir (the artifact's from)") + binary := fs.String("binary", "", "the binary's name in the archive") + system := fs.String("system", "", "the system stamped into main.builtFor (an artifact's system, e.g. arch)") + pack := fs.String("pack", "", "pack this directory as it is (a files archive) instead of building") + outFile := fs.String("out", "", "write the archive here") + if err := fs.Parse(args); err != nil { + return "", err + } + root := *pack + if root == "" { + if *from == "" || *binary == "" { + return "", fmt.Errorf("-from and -binary are needed (or -pack )") + } + out, err := Build(*dir, *from, *binary, *system) + if err != nil { + return "", err + } + defer os.RemoveAll(out) + root = out + } + archive, digest, err := buildstep.Pack(root) + if err != nil { + return "", err + } + if *outFile != "" { + if err := os.WriteFile(*outFile, archive, 0o644); err != nil { + return "", err + } + } + return digest, nil +} + +func main() { + digest, err := run(os.Args[1:]) + if err != nil { + fmt.Fprintln(os.Stderr, "mesh-pack:", err) + os.Exit(1) + } + fmt.Println(digest) +} diff --git a/modules/mesh-build-go/buildstep/cmd/mesh-pack/main_test.go b/modules/mesh-build-go/buildstep/cmd/mesh-pack/main_test.go new file mode 100644 index 00000000..0c866886 --- /dev/null +++ b/modules/mesh-build-go/buildstep/cmd/mesh-pack/main_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" +) + +func fixture(t *testing.T) string { + t.Helper() + d := t.TempDir() + os.MkdirAll(filepath.Join(d, "sub"), 0o755) + os.WriteFile(filepath.Join(d, "a.txt"), []byte("alpha"), 0o644) + os.WriteFile(filepath.Join(d, "sub", "run"), []byte("#!/bin/sh\n"), 0o755) + return d +} + +func TestPackFixtureTwiceGivesBuildstepsDigest(t *testing.T) { + d := fixture(t) + _, want, err := buildstep.Pack(d) + if err != nil { + t.Fatal(err) + } + for i := 0; i < 2; i++ { + got, err := run([]string{"-pack", d, "-out", filepath.Join(t.TempDir(), "x.tgz")}) + if err != nil || got != want { + t.Fatalf("run %d: %q, %v; want %q", i, got, err, want) + } + } +} + +func TestBuildTwiceGivesOneDigestAndRecipeArgs(t *testing.T) { + m := t.TempDir() + os.WriteFile(filepath.Join(m, "go.mod"), []byte("module x\n\ngo 1.21\n"), 0o644) + os.MkdirAll(filepath.Join(m, "cmd", "p"), 0o755) + os.WriteFile(filepath.Join(m, "cmd", "p", "main.go"), []byte("package main\n\nvar builtFor string\n\nfunc main() { println(builtFor) }\n"), 0o644) + args := []string{"-dir", m, "-from", "cmd/p", "-binary", "p", "-system", "arch"} + a, err := run(args) + if err != nil { + t.Skipf("no go toolchain that builds offline: %v", err) + } + b, err := run(args) + if err != nil || a != b { + t.Fatalf("two builds: %q, %q, %v", a, b, err) + } + got := Args("/d", "./cmd/p/", "/o", "p", "arch") + if got[len(got)-1] != "./cmd/p" || got[4] != "-s -w -X main.builtFor=arch" { + t.Fatalf("args %v", got) + } +} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/README.md b/modules/mesh-build-go/buildstep/conformance/captured/README.md new file mode 100644 index 00000000..3c882c93 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/README.md @@ -0,0 +1,73 @@ +# The conformance kit's captured runner output + +Every file here is the real output of a test runner, never written by hand (issue 1901: a fixture that agrees with the +mistake proves the mistake). The toolchains are the build nodes' own, as `mesh-build-go.build_go_toolchains` and +`mesh-build-node.build_node_toolchains` reported them on 2026-10-26: go1.27.3-X:nodwarf5 on every build node, node +v26.12.1 (npm 12.2.1) on shanks, novox and g14, and typescript 5.9.3 from mesh-build-node's toolchain/package.json. +To capture a case again, make its tiny project in a fresh directory as below and run the command shown; the kit's case +table (conformance.go) says what each failure must say. + +## go/ — `go test -json` + +- `passfirst.jsonl` and `subtests.jsonl` (2026-10-26, go1.27.3-X:nodwarf5, for issue 1901's thorough fix): in + `/tmp/1901k/go/`, `GOTOOLCHAIN=local GOFLAGS=-mod=mod go test -json -count=1 ./... > ../.jsonl 2>&1`. + - passfirst: module `example.com/passfirst`, `go 1.26`. Package `a` has `TestTheFirstPackagePasses` (empty). Package `b` + has `TestTheSecondPackagePassesFirst` (empty) and `TestTheSecondPackageFails`, which calls + `t.Logf("judged against the seat as it stands")` then `t.Fatalf("the seat has 2 holders, want 1")`: a passing package + and a passing test before the failing one. + - subtests: module `example.com/subtests`; `TestTheTable` runs `t.Run("passes", func(t *testing.T) {})` and + `t.Run("fails", func(t *testing.T) { t.Errorf("got 3 machines, want 4") })`: the parent fails only by its subtest. +- `subpanic.jsonl` (2026-10-26, go1.27.3-X:nodwarf5, review round 1 of issue 1901's fix), the same way: module + `example.com/subpanic`; `TestTable` runs `t.Run("passes", func(t *testing.T) {})` and `t.Run("panics", ...)`, which + writes to a nil map. go test -json files the subtest's panic under the parent `TestTable`. +- `logthenfatal.jsonl`, `panicking`, `timeout`, `deeptimeout`, `buildfail`, `exiting`, `killed`, `passthenfail`, + `mainexit`, `race`, `racemain` were captured for issue 1901's first and third meetings (2026-10-23 and 2026-10-25, + go1.27.3) in mesh-catalog's mesh-build-go (`testdata/captured/go-test-json-*.jsonl`, named there `go-test-json-pkg-` + and `go-test-json-log-then-fatal`) and moved here unchanged. From that README: each is the standard output and error of + `go test -json -count=1 ./...` (the timeouts with `-timeout 2s`; race and racemain with `-race`, CGO_ENABLED=1) in a + throwaway one-file module that fails without a test's own assertion: panicking (a nil map write in TestPanics), timeout + (TestWaitsForever sleeps past the timeout), deeptimeout (two parallel tests and sixty blocked goroutines past the + timeout, so the goroutine dump pushes the panic line out of the last 4 KB), buildfail (a type error in a test file), + exiting (os.Exit(3) in TestExits), killed (TestKilled sends itself SIGKILL), passthenfail (TestMain panics in a + goroutine after m.Run passed), mainexit (TestMain exits 2 after m.Run passed: go test prints "PASS" and then + "FAIL "), race (TestRacesOnTheSeat writes a package variable from itself and a goroutine), racemain (TestMain does + so after m.Run passed). logthenfatal is a test `TestThree` in package `x` that logs a line with t.Logf and then fails + with a multi-line t.Fatalf naming three subscriptions. + +## node/ — `npm test` running `node --test` under the TAP reporter + +Captured 2026-10-26 with node v26.12.1 and npm 12.2.1 (`/usr/bin/node`, `/usr/bin/npm`), in `/tmp/1901k/node/`, the +way mesh-build-node runs a test script: + + PATH=/usr/bin:$PATH NODE_OPTIONS="--test-reporter=tap --test-reporter-destination=/tmp/1901k/node/.tap" \ + npm test > ../.stdout 2> ../.stderr + +`.tap` is the report file, `.console` the stdout followed by the stderr. Each project's package.json is +`{"name":"","private":true,"type":"module","scripts":{"test":"node --test --experimental-strip-types 'test/*.test.ts'"}}` +(the script every TypeScript package of the catalogue and the SDK runs), with these test files: + +- two-files: `test/a.test.ts` holds `test('passes in the first file', () => {})`; `test/b.test.ts` holds + `test('passes in the second file first', () => {})` and `test('fails in the second file', ...)`, which logs + `judged against the seat as it stands` and asserts `assert.strictEqual(1 + 1, 3, 'one plus one is not three')`. +- suite: `test/a.test.ts` has `describe('the seat', ...)` with `it('passes inside', () => {})` and `it('fails inside', ...)` + asserting `assert.deepStrictEqual({ holders: 2 }, { holders: 1 })`. +- crash: `test/a.test.ts` holds `test('passes before the crash', () => {})`; `test/b.test.ts` holds + `test('throws after it returned', ...)`, which throws `new Error('the bus closed under the test')` in a `setTimeout` of + 1 ms, and `test('waits after the throw', ...)`, which waits 50 ms: node fails the file with an uncaught exception. +- logcrash (review round 1): `test/a.test.ts` holds `test('passes and logs', ...)`, which logs + `retrying after Error: the store was not ready yet`; `test/b.test.ts` is crash's. A passing test's console line + holding "Error" comes before the crash's own. +- timeout: `test/a.test.ts` holds `test('passes before the wait', () => {})` and + `test('waits for an answer that never comes', { timeout: 200 }, async () => { await new Promise(() => {}); })`. +- syntax: `test/a.test.ts` holds `test('passes in the first file', () => {})`; `test/b.test.ts` is + `import test from 'node:test';` and `test('never loads', () => {` (cut off): the file does not load. + +## tsc/ — the type-check + +`typeerror.console` (2026-10-26, typescript 5.9.3 under node v26.12.1): in `/tmp/1901k/tsc/typeerror`, `ok.ts` is +`export const holders: number = 1;` and `seat.ts` is `import { holders } from './ok.js'; export const name: string = holders;`; +the command is mesh-build-node's type-check line without a tsconfig.json: + + node /bin/tsc --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 --rootDir . ok.ts seat.ts + +and the file is its stdout followed by its stderr. diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/buildfail.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/buildfail.jsonl new file mode 100644 index 00000000..2b7967da --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/buildfail.jsonl @@ -0,0 +1,6 @@ +{"ImportPath":"example.com/buildfail [example.com/buildfail.test]","Action":"build-output","Output":"# example.com/buildfail [example.com/buildfail.test]\n"} +{"ImportPath":"example.com/buildfail [example.com/buildfail.test]","Action":"build-output","Output":"./p_test.go:5:40: cannot use \"seat\" (untyped string constant) as int value in variable declaration\n"} +{"ImportPath":"example.com/buildfail [example.com/buildfail.test]","Action":"build-fail"} +{"Time":"2026-10-25T15:34:33.261137737+01:00","Action":"start","Package":"example.com/buildfail"} +{"Time":"2026-10-25T15:34:33.261255019+01:00","Action":"output","Package":"example.com/buildfail","Output":"FAIL\texample.com/buildfail [build failed]\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.261268099+01:00","Action":"fail","Package":"example.com/buildfail","Elapsed":0,"FailedBuild":"example.com/buildfail [example.com/buildfail.test]"} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/deeptimeout.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/deeptimeout.jsonl new file mode 100644 index 00000000..40182508 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/deeptimeout.jsonl @@ -0,0 +1,417 @@ +{"Time":"2026-10-25T15:35:18.991630993+01:00","Action":"start","Package":"example.com/deeptimeout"} +{"Time":"2026-10-25T15:35:18.993895188+01:00","Action":"run","Package":"example.com/deeptimeout","Test":"TestHoldsTheSeat"} +{"Time":"2026-10-25T15:35:18.993963909+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestHoldsTheSeat","Output":"=== RUN TestHoldsTheSeat\n","OutputType":"frame"} +{"Time":"2026-10-25T15:35:18.99399484+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestHoldsTheSeat","Output":"=== PAUSE TestHoldsTheSeat\n","OutputType":"frame"} +{"Time":"2026-10-25T15:35:18.9939982+01:00","Action":"pause","Package":"example.com/deeptimeout","Test":"TestHoldsTheSeat"} +{"Time":"2026-10-25T15:35:18.99400173+01:00","Action":"run","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus"} +{"Time":"2026-10-25T15:35:18.99400456+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"=== RUN TestWaitsForTheBus\n","OutputType":"frame"} +{"Time":"2026-10-25T15:35:18.99400838+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"=== PAUSE TestWaitsForTheBus\n","OutputType":"frame"} +{"Time":"2026-10-25T15:35:18.99401133+01:00","Action":"pause","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus"} +{"Time":"2026-10-25T15:35:18.99401441+01:00","Action":"cont","Package":"example.com/deeptimeout","Test":"TestHoldsTheSeat"} +{"Time":"2026-10-25T15:35:18.99401704+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestHoldsTheSeat","Output":"=== CONT TestHoldsTheSeat\n","OutputType":"frame"} +{"Time":"2026-10-25T15:35:18.994074141+01:00","Action":"cont","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus"} +{"Time":"2026-10-25T15:35:18.994102612+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"=== CONT TestWaitsForTheBus\n","OutputType":"frame"} +{"Time":"2026-10-25T15:35:21.00935195+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"panic: test timed out after 2s\n"} +{"Time":"2026-10-25T15:35:21.00938383+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\trunning tests:\n"} +{"Time":"2026-10-25T15:35:21.009394391+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t\tTestHoldsTheSeat (2s)\n"} +{"Time":"2026-10-25T15:35:21.009398021+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t\tTestWaitsForTheBus (2s)\n"} +{"Time":"2026-10-25T15:35:21.009401801+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009406481+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 81 [running]:\n"} +{"Time":"2026-10-25T15:35:21.009409911+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"testing.(*M).startAlarm.func1()\n"} +{"Time":"2026-10-25T15:35:21.009413361+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/testing/testing.go:2961 +0x34a\n"} +{"Time":"2026-10-25T15:35:21.009418341+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by time.goFunc\n"} +{"Time":"2026-10-25T15:35:21.009421781+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/time/sleep.go:182 +0x2d\n"} +{"Time":"2026-10-25T15:35:21.009424991+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009428341+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 1 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009431721+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"testing.tRunner.func1()\n"} +{"Time":"2026-10-25T15:35:21.009441811+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/testing/testing.go:2144 +0x425\n"} +{"Time":"2026-10-25T15:35:21.009445511+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"testing.tRunner(0x282e9702008, 0x282e9646bc8)\n"} +{"Time":"2026-10-25T15:35:21.009449641+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/testing/testing.go:2201 +0x123\n"} +{"Time":"2026-10-25T15:35:21.009453471+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"testing.runTests({0x55a45d, 0x17}, {0x55a45d, 0x17}, 0x282e95940f0, {0x6f19f0, 0x2, 0x2}, {0xc2afa2ca3b3c0c8d, 0x7737f168, ...})\n"} +{"Time":"2026-10-25T15:35:21.009462092+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/testing/testing.go:2742 +0x510\n"} +{"Time":"2026-10-25T15:35:21.009467222+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"testing.(*M).Run(0x282e96581e0)\n"} +{"Time":"2026-10-25T15:35:21.009470662+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/testing/testing.go:2602 +0x6af\n"} +{"Time":"2026-10-25T15:35:21.009473692+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"main.main()\n"} +{"Time":"2026-10-25T15:35:21.009476942+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t_testmain.go:48 +0x9b\n"} +{"Time":"2026-10-25T15:35:21.009480092+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009483242+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 7 [sleep]:\n"} +{"Time":"2026-10-25T15:35:21.009486502+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"time.Sleep(0xdf8475800)\n"} +{"Time":"2026-10-25T15:35:21.009490682+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/runtime/time.go:368 +0x165\n"} +{"Time":"2026-10-25T15:35:21.009493812+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat(0x282e9702248?)\n"} +{"Time":"2026-10-25T15:35:21.009498842+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:14 +0x9e\n"} +{"Time":"2026-10-25T15:35:21.009502242+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"testing.tRunner(0x282e9702248, 0x6d5e78)\n"} +{"Time":"2026-10-25T15:35:21.009505742+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/testing/testing.go:2195 +0xea\n"} +{"Time":"2026-10-25T15:35:21.009509292+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by testing.(*T).Run in goroutine 1\n"} +{"Time":"2026-10-25T15:35:21.009512752+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/testing/testing.go:2260 +0x4d4\n"} +{"Time":"2026-10-25T15:35:21.009515792+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009518623+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 8 [sleep]:\n"} +{"Time":"2026-10-25T15:35:21.009521303+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"time.Sleep(0xdf8475800)\n"} +{"Time":"2026-10-25T15:35:21.009524423+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/runtime/time.go:368 +0x165\n"} +{"Time":"2026-10-25T15:35:21.009531403+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestWaitsForTheBus(0x282e9702488?)\n"} +{"Time":"2026-10-25T15:35:21.009534943+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:19 +0x25\n"} +{"Time":"2026-10-25T15:35:21.009538403+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"testing.tRunner(0x282e9702488, 0x6d5e80)\n"} +{"Time":"2026-10-25T15:35:21.009541953+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/testing/testing.go:2195 +0xea\n"} +{"Time":"2026-10-25T15:35:21.009545233+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by testing.(*T).Run in goroutine 1\n"} +{"Time":"2026-10-25T15:35:21.009548323+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/usr/lib/go/src/testing/testing.go:2260 +0x4d4\n"} +{"Time":"2026-10-25T15:35:21.009551343+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009554263+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 9 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009557523+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009561063+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009564513+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009571113+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009574233+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009577433+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 10 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009580823+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009584174+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009587504+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009590974+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009594004+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009597184+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 11 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009600394+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009603874+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009610954+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009614714+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009618104+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009621364+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 12 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009624704+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009628324+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009631954+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009635404+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009638734+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009641904+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 13 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009645325+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009648645+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009652065+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009655625+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009658885+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009663385+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 14 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009666735+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009670235+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009676385+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009680205+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009683315+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009686475+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 15 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009689785+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009697235+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009700795+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009704985+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009708085+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009711346+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 16 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009714586+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009717946+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009721676+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009725626+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009728866+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009732106+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 17 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009735376+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009738956+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009742326+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009746436+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009750666+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009754126+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 18 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009758286+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009761846+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009765776+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009769816+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009773167+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009780507+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 19 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009784207+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009788007+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009791627+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009796347+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009799427+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009802937+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 20 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009806667+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009809987+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009813827+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009818087+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009821397+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009824227+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 21 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009827607+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009831307+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009835167+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009839528+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009842728+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009845888+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 22 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009849168+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009852548+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009855958+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009863648+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009866938+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009870088+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 23 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009879828+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009883728+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009887608+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009891548+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009895238+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009898748+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 24 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009902349+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009907109+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009911519+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009915609+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009918939+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009922329+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 25 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009928329+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009931909+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.009935489+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.009939429+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.009942699+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.009946019+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 26 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.009949559+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.009953489+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.00996244+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.00996647+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.00996987+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.00997314+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 27 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.00997664+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.00998035+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.00998418+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.00998793+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.00999131+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.00999534+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 28 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.00999894+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.01000268+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.0100087+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.01001258+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.01001605+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.01002007+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 29 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.01002393+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010027531+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010031351+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010035521+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010038781+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010042281+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 30 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010046091+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010054261+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010057881+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010061781+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010064931+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010068261+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 31 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010072151+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010075981+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010079841+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010098802+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010103202+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010106382+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 32 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010109852+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010113242+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010116952+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010120672+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010124112+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010127162+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 33 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010130662+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010134442+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010138142+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010141972+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010145362+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010152262+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 34 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010156353+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010159913+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010163533+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010167403+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010170533+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010173953+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 35 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010177453+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010181033+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010184693+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010188583+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010191753+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010195033+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 36 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010198603+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010202393+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010206123+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010210583+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010213503+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010216914+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 37 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010220134+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010223644+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010227154+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010235154+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010238354+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010241754+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 38 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010270284+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010275144+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010278714+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010282825+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010285795+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010288935+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 39 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010292405+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010295915+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010299545+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010311835+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010315565+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010318975+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 40 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010322655+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010326225+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010329815+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010333745+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010337095+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010340515+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 41 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010344066+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010347706+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010355596+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010359046+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010362036+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010365076+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 42 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010367956+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010370906+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010375366+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010378386+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010381006+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010383666+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 43 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010386676+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010389786+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010392636+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010395746+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010398466+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010401166+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 44 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010404226+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010407186+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010410117+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010413237+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010416157+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010418897+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 45 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010423157+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010430107+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010433197+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010451207+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010454217+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010456967+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 46 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010459877+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010463967+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010467067+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010470597+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010473418+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010476108+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 47 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010478998+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010481958+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010484978+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010488258+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010490888+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010493628+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 48 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010496588+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010499498+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010502498+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010505768+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010508558+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010514148+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 49 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010517178+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010520238+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010523478+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010526818+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010529718+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010532508+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 50 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010535639+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010538749+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010541939+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010546689+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010549869+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010552989+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 51 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010556299+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010560079+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010563479+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010567189+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010570299+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010573079+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 52 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010576319+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010579599+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010583099+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010590519+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010594029+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010596989+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 53 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.01059986+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.01060433+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.01060789+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.01061136+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.01061393+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.01061667+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 54 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.01061984+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.0106227+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.01062566+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.01062876+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.01063131+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.01063428+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 55 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.01063719+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.01063997+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.01064278+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.01064582+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.01064836+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.01065114+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 56 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.01065437+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.01065724+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010671481+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010675671+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010678431+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010691171+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 57 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010694531+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010697641+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010700781+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010704001+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010706611+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010709421+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 58 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010712331+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010715351+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010718431+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010721691+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010724371+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010727362+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 59 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010730592+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010733662+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010736712+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010740162+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010742922+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010745772+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 60 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010752222+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010755402+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010758542+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010761822+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010764592+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010767992+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 61 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010771062+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010774282+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010779382+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010782942+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010785722+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010788573+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 62 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010791893+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010794943+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010798083+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010801683+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010804293+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010807283+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 63 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010810443+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010813553+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010816793+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010820233+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010823063+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010829493+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 64 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010832683+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010835953+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010839223+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010842463+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010845333+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010848423+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 65 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010851663+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010854964+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010858364+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010861714+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010864914+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010867984+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 66 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010870944+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010874114+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010877464+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010880504+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010883144+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010886194+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 67 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010889154+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010892234+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010895524+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010902154+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.010904984+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\n"} +{"Time":"2026-10-25T15:35:21.010907984+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"goroutine 68 [chan receive]:\n"} +{"Time":"2026-10-25T15:35:21.010911064+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"example.com/deeptimeout.TestHoldsTheSeat.func1()\n"} +{"Time":"2026-10-25T15:35:21.010914074+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x19\n"} +{"Time":"2026-10-25T15:35:21.010917365+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"created by example.com/deeptimeout.TestHoldsTheSeat in goroutine 7\n"} +{"Time":"2026-10-25T15:35:21.010920505+01:00","Action":"output","Package":"example.com/deeptimeout","Test":"TestWaitsForTheBus","Output":"\t/tmp/fix-1901b/deeptimeout/p_test.go:12 +0x37\n"} +{"Time":"2026-10-25T15:35:21.013209581+01:00","Action":"output","Package":"example.com/deeptimeout","Output":"FAIL\texample.com/deeptimeout\t2.021s\n","OutputType":"frame"} +{"Time":"2026-10-25T15:35:21.013258851+01:00","Action":"fail","Package":"example.com/deeptimeout","Elapsed":2.022} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/exiting.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/exiting.jsonl new file mode 100644 index 00000000..ec4ce02f --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/exiting.jsonl @@ -0,0 +1,10 @@ +{"Time":"2026-10-25T15:34:33.768742837+01:00","Action":"start","Package":"example.com/exiting"} +{"Time":"2026-10-25T15:34:33.773822287+01:00","Action":"run","Package":"example.com/exiting","Test":"TestFine"} +{"Time":"2026-10-25T15:34:33.773896608+01:00","Action":"output","Package":"example.com/exiting","Test":"TestFine","Output":"=== RUN TestFine\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.773940319+01:00","Action":"output","Package":"example.com/exiting","Test":"TestFine","Output":"--- PASS: TestFine (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.773945359+01:00","Action":"pass","Package":"example.com/exiting","Test":"TestFine","Elapsed":0} +{"Time":"2026-10-25T15:34:33.773952529+01:00","Action":"run","Package":"example.com/exiting","Test":"TestExits"} +{"Time":"2026-10-25T15:34:33.773956439+01:00","Action":"output","Package":"example.com/exiting","Test":"TestExits","Output":"=== RUN TestExits\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.773959829+01:00","Action":"output","Package":"example.com/exiting","Test":"TestExits","Output":" p_test.go:11: closing the store\n"} +{"Time":"2026-10-25T15:34:33.775158538+01:00","Action":"output","Package":"example.com/exiting","Output":"FAIL\texample.com/exiting\t0.006s\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.775186509+01:00","Action":"fail","Package":"example.com/exiting","Elapsed":0.006} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/killed.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/killed.jsonl new file mode 100644 index 00000000..f4ba5258 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/killed.jsonl @@ -0,0 +1,7 @@ +{"Time":"2026-10-25T15:34:34.328258934+01:00","Action":"start","Package":"example.com/killed"} +{"Time":"2026-10-25T15:34:34.331465435+01:00","Action":"run","Package":"example.com/killed","Test":"TestKilled"} +{"Time":"2026-10-25T15:34:34.331699248+01:00","Action":"output","Package":"example.com/killed","Test":"TestKilled","Output":"=== RUN TestKilled\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:34.331853111+01:00","Action":"output","Package":"example.com/killed","Test":"TestKilled","Output":" p_test.go:10: the kernel is about to kill this\n"} +{"Time":"2026-10-25T15:34:34.334563213+01:00","Action":"output","Package":"example.com/killed","Test":"TestKilled","Output":"signal: killed\n"} +{"Time":"2026-10-25T15:34:34.334611114+01:00","Action":"output","Package":"example.com/killed","Output":"FAIL\texample.com/killed\t0.006s\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:34.334627134+01:00","Action":"fail","Package":"example.com/killed","Elapsed":0.006} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/logthenfatal.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/logthenfatal.jsonl new file mode 100644 index 00000000..c59fba2f --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/logthenfatal.jsonl @@ -0,0 +1,13 @@ +{"Action":"start","Package":"x"} +{"Action":"run","Package":"x","Test":"TestThree"} +{"Action":"output","Package":"x","Test":"TestThree","Output":"=== RUN TestThree\n","OutputType":"frame"} +{"Action":"output","Package":"x","Test":"TestThree","Output":" a_test.go:4: judged against /var/lib/mesh-build-go-work/id/beside/mesh-catalog as cloned\n"} +{"Action":"output","Package":"x","Test":"TestThree","Output":" a_test.go:5: 3 subscription(s) derive to a subject no emitter owns:\n","OutputType":"error"} +{"Action":"output","Package":"x","Test":"TestThree","Output":" a.b.c\n","OutputType":"error-continue"} +{"Action":"output","Package":"x","Test":"TestThree","Output":" d.e.f\n","OutputType":"error-continue"} +{"Action":"output","Package":"x","Test":"TestThree","Output":" g.h.i\n","OutputType":"error-continue"} +{"Action":"output","Package":"x","Test":"TestThree","Output":"--- FAIL: TestThree (0.00s)\n","OutputType":"frame"} +{"Action":"fail","Package":"x","Test":"TestThree","Elapsed":0} +{"Action":"output","Package":"x","Output":"FAIL\n","OutputType":"frame"} +{"Action":"output","Package":"x","Output":"FAIL\tx\t0.002s\n","OutputType":"frame"} +{"Action":"fail","Package":"x","Elapsed":0.002} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/mainexit.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/mainexit.jsonl new file mode 100644 index 00000000..5f106bad --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/mainexit.jsonl @@ -0,0 +1,8 @@ +{"Time":"2026-10-25T15:34:35.274953775+01:00","Action":"start","Package":"example.com/mainexit"} +{"Time":"2026-10-25T15:34:35.278129565+01:00","Action":"run","Package":"example.com/mainexit","Test":"TestFine"} +{"Time":"2026-10-25T15:34:35.278191516+01:00","Action":"output","Package":"example.com/mainexit","Test":"TestFine","Output":"=== RUN TestFine\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:35.278341979+01:00","Action":"output","Package":"example.com/mainexit","Test":"TestFine","Output":"--- PASS: TestFine (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:35.278351739+01:00","Action":"pass","Package":"example.com/mainexit","Test":"TestFine","Elapsed":0} +{"Time":"2026-10-25T15:34:35.278359039+01:00","Action":"output","Package":"example.com/mainexit","Output":"PASS\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:35.279516487+01:00","Action":"output","Package":"example.com/mainexit","Output":"FAIL\texample.com/mainexit\t0.004s\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:35.279539298+01:00","Action":"fail","Package":"example.com/mainexit","Elapsed":0.005} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/panicking.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/panicking.jsonl new file mode 100644 index 00000000..7609d5d2 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/panicking.jsonl @@ -0,0 +1,27 @@ +{"Time":"2026-10-25T15:34:33.031091436+01:00","Action":"start","Package":"example.com/panicking"} +{"Time":"2026-10-25T15:34:33.036353279+01:00","Action":"run","Package":"example.com/panicking","Test":"TestBefore"} +{"Time":"2026-10-25T15:34:33.036441781+01:00","Action":"output","Package":"example.com/panicking","Test":"TestBefore","Output":"=== RUN TestBefore\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.036568163+01:00","Action":"output","Package":"example.com/panicking","Test":"TestBefore","Output":"--- PASS: TestBefore (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.036609863+01:00","Action":"pass","Package":"example.com/panicking","Test":"TestBefore","Elapsed":0} +{"Time":"2026-10-25T15:34:33.036641304+01:00","Action":"run","Package":"example.com/panicking","Test":"TestPanics"} +{"Time":"2026-10-25T15:34:33.036645914+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"=== RUN TestPanics\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.036698995+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":" p_test.go:8: about to look up the seat\n"} +{"Time":"2026-10-25T15:34:33.036773996+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"--- FAIL: TestPanics (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.03897418+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"panic: assignment to entry in nil map [recovered, repanicked]\n"} +{"Time":"2026-10-25T15:34:33.040332172+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"\n"} +{"Time":"2026-10-25T15:34:33.040414493+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"goroutine 8 [running]:\n"} +{"Time":"2026-10-25T15:34:33.040521475+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"testing.tRunner.func1.2({0x6b8120, 0x6eff80})\n"} +{"Time":"2026-10-25T15:34:33.040601826+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"\t/usr/lib/go/src/testing/testing.go:2125 +0x232\n"} +{"Time":"2026-10-25T15:34:33.040641947+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"testing.tRunner.func1()\n"} +{"Time":"2026-10-25T15:34:33.040720648+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"\t/usr/lib/go/src/testing/testing.go:2128 +0x329\n"} +{"Time":"2026-10-25T15:34:33.04083286+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"panic({0x6b8120?, 0x6eff80?})\n"} +{"Time":"2026-10-25T15:34:33.040912921+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"\t/usr/lib/go/src/runtime/panic.go:859 +0x125\n"} +{"Time":"2026-10-25T15:34:33.040981652+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"example.com/panicking.TestPanics(0x228226cd0488?)\n"} +{"Time":"2026-10-25T15:34:33.041059903+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"\t/tmp/fix-1901b/panicking/p_test.go:10 +0x53\n"} +{"Time":"2026-10-25T15:34:33.041132704+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"testing.tRunner(0x228226cd0488, 0x6d5bc8)\n"} +{"Time":"2026-10-25T15:34:33.041215586+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"\t/usr/lib/go/src/testing/testing.go:2195 +0xea\n"} +{"Time":"2026-10-25T15:34:33.041221966+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"created by testing.(*T).Run in goroutine 1\n"} +{"Time":"2026-10-25T15:34:33.041225556+01:00","Action":"output","Package":"example.com/panicking","Test":"TestPanics","Output":"\t/usr/lib/go/src/testing/testing.go:2260 +0x4d4\n"} +{"Time":"2026-10-25T15:34:33.041984088+01:00","Action":"fail","Package":"example.com/panicking","Test":"TestPanics","Elapsed":0} +{"Time":"2026-10-25T15:34:33.041996638+01:00","Action":"output","Package":"example.com/panicking","Output":"FAIL\texample.com/panicking\t0.010s\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:33.042044079+01:00","Action":"fail","Package":"example.com/panicking","Elapsed":0.011} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/passfirst.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/passfirst.jsonl new file mode 100644 index 00000000..a8286d9c --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/passfirst.jsonl @@ -0,0 +1,22 @@ +{"Time":"2026-10-26T19:40:29.574068887+01:00","Action":"start","Package":"example.com/passfirst/a"} +{"Time":"2026-10-26T19:40:29.574347185+01:00","Action":"start","Package":"example.com/passfirst/b"} +{"Time":"2026-10-26T19:40:29.576503518+01:00","Action":"run","Package":"example.com/passfirst/a","Test":"TestTheFirstPackagePasses"} +{"Time":"2026-10-26T19:40:29.576557657+01:00","Action":"output","Package":"example.com/passfirst/a","Test":"TestTheFirstPackagePasses","Output":"=== RUN TestTheFirstPackagePasses\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.576589367+01:00","Action":"output","Package":"example.com/passfirst/a","Test":"TestTheFirstPackagePasses","Output":"--- PASS: TestTheFirstPackagePasses (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.576594637+01:00","Action":"pass","Package":"example.com/passfirst/a","Test":"TestTheFirstPackagePasses","Elapsed":0} +{"Time":"2026-10-26T19:40:29.576602487+01:00","Action":"output","Package":"example.com/passfirst/a","Output":"PASS\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.576614477+01:00","Action":"run","Package":"example.com/passfirst/b","Test":"TestTheSecondPackagePassesFirst"} +{"Time":"2026-10-26T19:40:29.576626897+01:00","Action":"output","Package":"example.com/passfirst/b","Test":"TestTheSecondPackagePassesFirst","Output":"=== RUN TestTheSecondPackagePassesFirst\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.576631187+01:00","Action":"output","Package":"example.com/passfirst/b","Test":"TestTheSecondPackagePassesFirst","Output":"--- PASS: TestTheSecondPackagePassesFirst (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.576635567+01:00","Action":"pass","Package":"example.com/passfirst/b","Test":"TestTheSecondPackagePassesFirst","Elapsed":0} +{"Time":"2026-10-26T19:40:29.576638427+01:00","Action":"run","Package":"example.com/passfirst/b","Test":"TestTheSecondPackageFails"} +{"Time":"2026-10-26T19:40:29.576640647+01:00","Action":"output","Package":"example.com/passfirst/b","Test":"TestTheSecondPackageFails","Output":"=== RUN TestTheSecondPackageFails\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.576643477+01:00","Action":"output","Package":"example.com/passfirst/b","Test":"TestTheSecondPackageFails","Output":" b_test.go:8: judged against the seat as it stands\n"} +{"Time":"2026-10-26T19:40:29.576646497+01:00","Action":"output","Package":"example.com/passfirst/b","Test":"TestTheSecondPackageFails","Output":" b_test.go:9: the seat has 2 holders, want 1\n","OutputType":"error"} +{"Time":"2026-10-26T19:40:29.576650087+01:00","Action":"output","Package":"example.com/passfirst/b","Test":"TestTheSecondPackageFails","Output":"--- FAIL: TestTheSecondPackageFails (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.576652797+01:00","Action":"fail","Package":"example.com/passfirst/b","Test":"TestTheSecondPackageFails","Elapsed":0} +{"Time":"2026-10-26T19:40:29.576655177+01:00","Action":"output","Package":"example.com/passfirst/b","Output":"FAIL\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.577365541+01:00","Action":"output","Package":"example.com/passfirst/a","Output":"ok \texample.com/passfirst/a\t0.003s\n"} +{"Time":"2026-10-26T19:40:29.577382561+01:00","Action":"pass","Package":"example.com/passfirst/a","Elapsed":0.003} +{"Time":"2026-10-26T19:40:29.577380641+01:00","Action":"output","Package":"example.com/passfirst/b","Output":"FAIL\texample.com/passfirst/b\t0.003s\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.5774131+01:00","Action":"fail","Package":"example.com/passfirst/b","Elapsed":0.003} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/passthenfail.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/passthenfail.jsonl new file mode 100644 index 00000000..55fa9309 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/passthenfail.jsonl @@ -0,0 +1,15 @@ +{"Time":"2026-10-25T15:34:34.919624192+01:00","Action":"start","Package":"example.com/passthenfail"} +{"Time":"2026-10-25T15:34:34.922861224+01:00","Action":"run","Package":"example.com/passthenfail","Test":"TestFine"} +{"Time":"2026-10-25T15:34:34.922932965+01:00","Action":"output","Package":"example.com/passthenfail","Test":"TestFine","Output":"=== RUN TestFine\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:34.922969205+01:00","Action":"output","Package":"example.com/passthenfail","Test":"TestFine","Output":"--- PASS: TestFine (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:34.922979956+01:00","Action":"pass","Package":"example.com/passthenfail","Test":"TestFine","Elapsed":0} +{"Time":"2026-10-25T15:34:34.922989416+01:00","Action":"output","Package":"example.com/passthenfail","Output":"PASS\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:34.925022528+01:00","Action":"output","Package":"example.com/passthenfail","Output":"panic: the bus closed after the tests: nats: connection closed\n"} +{"Time":"2026-10-25T15:34:34.925043388+01:00","Action":"output","Package":"example.com/passthenfail","Output":"\n"} +{"Time":"2026-10-25T15:34:34.925046848+01:00","Action":"output","Package":"example.com/passthenfail","Output":"goroutine 20 [running]:\n"} +{"Time":"2026-10-25T15:34:34.925051338+01:00","Action":"output","Package":"example.com/passthenfail","Output":"example.com/passthenfail.TestMain.func1()\n"} +{"Time":"2026-10-25T15:34:34.925055878+01:00","Action":"output","Package":"example.com/passthenfail","Output":"\t/tmp/fix-1901b/passthenfail/p_test.go:13 +0x25\n"} +{"Time":"2026-10-25T15:34:34.925061428+01:00","Action":"output","Package":"example.com/passthenfail","Output":"created by example.com/passthenfail.TestMain in goroutine 1\n"} +{"Time":"2026-10-25T15:34:34.925065028+01:00","Action":"output","Package":"example.com/passthenfail","Output":"\t/tmp/fix-1901b/passthenfail/p_test.go:13 +0x25\n"} +{"Time":"2026-10-25T15:34:34.925390203+01:00","Action":"output","Package":"example.com/passthenfail","Output":"FAIL\texample.com/passthenfail\t0.005s\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:34.925402904+01:00","Action":"fail","Package":"example.com/passthenfail","Elapsed":0.006} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/race.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/race.jsonl new file mode 100644 index 00000000..93e9ac17 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/race.jsonl @@ -0,0 +1,53 @@ +{"Time":"2026-10-25T15:50:42.908557309+01:00","Action":"start","Package":"example.com/race"} +{"Time":"2026-10-25T15:50:42.924298679+01:00","Action":"run","Package":"example.com/race","Test":"TestBefore"} +{"Time":"2026-10-25T15:50:42.92438195+01:00","Action":"output","Package":"example.com/race","Test":"TestBefore","Output":"=== RUN TestBefore\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:42.924648842+01:00","Action":"output","Package":"example.com/race","Test":"TestBefore","Output":"--- PASS: TestBefore (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:42.924685133+01:00","Action":"pass","Package":"example.com/race","Test":"TestBefore","Elapsed":0} +{"Time":"2026-10-25T15:50:42.924728573+01:00","Action":"run","Package":"example.com/race","Test":"TestRacesOnTheSeat"} +{"Time":"2026-10-25T15:50:42.924733573+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"=== RUN TestRacesOnTheSeat\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:42.92637298+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"==================\n"} +{"Time":"2026-10-25T15:50:42.92641567+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"WARNING: DATA RACE\n"} +{"Time":"2026-10-25T15:50:42.926444061+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"Write at 0x000000833468 by goroutine 10:\n"} +{"Time":"2026-10-25T15:50:42.926462621+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" example.com/race.TestRacesOnTheSeat.func1()\n"} +{"Time":"2026-10-25T15:50:42.926467121+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /tmp/fix-1901b/race/p_test.go:13 +0x24\n"} +{"Time":"2026-10-25T15:50:42.926483061+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"\n"} +{"Time":"2026-10-25T15:50:42.926509741+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"Previous write at 0x000000833468 by goroutine 9:\n"} +{"Time":"2026-10-25T15:50:42.926530142+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" example.com/race.TestRacesOnTheSeat()\n"} +{"Time":"2026-10-25T15:50:42.926534952+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /tmp/fix-1901b/race/p_test.go:14 +0x30\n"} +{"Time":"2026-10-25T15:50:42.926550462+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.tRunner()\n"} +{"Time":"2026-10-25T15:50:42.926554612+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /usr/lib/go/src/testing/testing.go:2195 +0x21c\n"} +{"Time":"2026-10-25T15:50:42.926571142+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.(*T).Run.gowrap1()\n"} +{"Time":"2026-10-25T15:50:42.926575812+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /usr/lib/go/src/testing/testing.go:2260 +0x38\n"} +{"Time":"2026-10-25T15:50:42.926592202+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"\n"} +{"Time":"2026-10-25T15:50:42.926607792+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"Goroutine 10 (running) created at:\n"} +{"Time":"2026-10-25T15:50:42.926624292+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" example.com/race.TestRacesOnTheSeat()\n"} +{"Time":"2026-10-25T15:50:42.926628923+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /tmp/fix-1901b/race/p_test.go:13 +0x24\n"} +{"Time":"2026-10-25T15:50:42.926643853+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.tRunner()\n"} +{"Time":"2026-10-25T15:50:42.926648103+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /usr/lib/go/src/testing/testing.go:2195 +0x21c\n"} +{"Time":"2026-10-25T15:50:42.926674843+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.(*T).Run.gowrap1()\n"} +{"Time":"2026-10-25T15:50:42.926680673+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /usr/lib/go/src/testing/testing.go:2260 +0x38\n"} +{"Time":"2026-10-25T15:50:42.926695433+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"\n"} +{"Time":"2026-10-25T15:50:42.926711063+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"Goroutine 9 (running) created at:\n"} +{"Time":"2026-10-25T15:50:42.926726614+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.(*T).Run()\n"} +{"Time":"2026-10-25T15:50:42.926730814+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /usr/lib/go/src/testing/testing.go:2260 +0xb12\n"} +{"Time":"2026-10-25T15:50:42.926746484+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.runTests.func1()\n"} +{"Time":"2026-10-25T15:50:42.926750764+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /usr/lib/go/src/testing/testing.go:2744 +0x84\n"} +{"Time":"2026-10-25T15:50:42.926765354+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.tRunner()\n"} +{"Time":"2026-10-25T15:50:42.926769844+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /usr/lib/go/src/testing/testing.go:2195 +0x21c\n"} +{"Time":"2026-10-25T15:50:42.926791794+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.runTests()\n"} +{"Time":"2026-10-25T15:50:42.926796704+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /usr/lib/go/src/testing/testing.go:2742 +0x9e9\n"} +{"Time":"2026-10-25T15:50:42.926811544+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.(*M).Run()\n"} +{"Time":"2026-10-25T15:50:42.926816694+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" /usr/lib/go/src/testing/testing.go:2602 +0xf44\n"} +{"Time":"2026-10-25T15:50:42.926831695+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" main.main()\n"} +{"Time":"2026-10-25T15:50:42.926835935+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" _testmain.go:50 +0x164\n"} +{"Time":"2026-10-25T15:50:42.926850905+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"==================\n"} +{"Time":"2026-10-25T15:50:42.935320241+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":" testing.go:1867: race detected during execution of test\n","OutputType":"error"} +{"Time":"2026-10-25T15:50:42.935520113+01:00","Action":"output","Package":"example.com/race","Test":"TestRacesOnTheSeat","Output":"--- FAIL: TestRacesOnTheSeat (0.01s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:42.935592973+01:00","Action":"fail","Package":"example.com/race","Test":"TestRacesOnTheSeat","Elapsed":0.01} +{"Time":"2026-10-25T15:50:42.935722225+01:00","Action":"run","Package":"example.com/race","Test":"TestAfter"} +{"Time":"2026-10-25T15:50:42.935728845+01:00","Action":"output","Package":"example.com/race","Test":"TestAfter","Output":"=== RUN TestAfter\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:42.935995928+01:00","Action":"output","Package":"example.com/race","Test":"TestAfter","Output":"--- PASS: TestAfter (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:42.936006328+01:00","Action":"pass","Package":"example.com/race","Test":"TestAfter","Elapsed":0} +{"Time":"2026-10-25T15:50:42.936009978+01:00","Action":"output","Package":"example.com/race","Output":"FAIL\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:42.937730745+01:00","Action":"output","Package":"example.com/race","Output":"FAIL\texample.com/race\t0.028s\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:42.937802066+01:00","Action":"fail","Package":"example.com/race","Elapsed":0.029} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/racemain.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/racemain.jsonl new file mode 100644 index 00000000..8a2a5c07 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/racemain.jsonl @@ -0,0 +1,27 @@ +{"Time":"2026-10-25T15:50:43.551701745+01:00","Action":"start","Package":"example.com/racemain"} +{"Time":"2026-10-25T15:50:43.570709708+01:00","Action":"run","Package":"example.com/racemain","Test":"TestFine"} +{"Time":"2026-10-25T15:50:43.570806059+01:00","Action":"output","Package":"example.com/racemain","Test":"TestFine","Output":"=== RUN TestFine\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:43.57085525+01:00","Action":"output","Package":"example.com/racemain","Test":"TestFine","Output":"--- PASS: TestFine (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:43.57086149+01:00","Action":"pass","Package":"example.com/racemain","Test":"TestFine","Elapsed":0} +{"Time":"2026-10-25T15:50:43.57086974+01:00","Action":"output","Package":"example.com/racemain","Output":"PASS\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:43.572155623+01:00","Action":"output","Package":"example.com/racemain","Output":"==================\n"} +{"Time":"2026-10-25T15:50:43.572168083+01:00","Action":"output","Package":"example.com/racemain","Output":"WARNING: DATA RACE\n"} +{"Time":"2026-10-25T15:50:43.572171933+01:00","Action":"output","Package":"example.com/racemain","Output":"Write at 0x000000836428 by goroutine 9:\n"} +{"Time":"2026-10-25T15:50:43.572176703+01:00","Action":"output","Package":"example.com/racemain","Output":" example.com/racemain.TestMain.func1()\n"} +{"Time":"2026-10-25T15:50:43.572180053+01:00","Action":"output","Package":"example.com/racemain","Output":" /tmp/fix-1901b/racemain/p_test.go:15 +0x24\n"} +{"Time":"2026-10-25T15:50:43.572184923+01:00","Action":"output","Package":"example.com/racemain","Output":"\n"} +{"Time":"2026-10-25T15:50:43.572189613+01:00","Action":"output","Package":"example.com/racemain","Output":"Previous write at 0x000000836428 by main goroutine:\n"} +{"Time":"2026-10-25T15:50:43.572192993+01:00","Action":"output","Package":"example.com/racemain","Output":" example.com/racemain.TestMain()\n"} +{"Time":"2026-10-25T15:50:43.572198573+01:00","Action":"output","Package":"example.com/racemain","Output":" /tmp/fix-1901b/racemain/p_test.go:16 +0x44\n"} +{"Time":"2026-10-25T15:50:43.572202433+01:00","Action":"output","Package":"example.com/racemain","Output":" main.main()\n"} +{"Time":"2026-10-25T15:50:43.572205623+01:00","Action":"output","Package":"example.com/racemain","Output":" _testmain.go:48 +0x171\n"} +{"Time":"2026-10-25T15:50:43.572208443+01:00","Action":"output","Package":"example.com/racemain","Output":"\n"} +{"Time":"2026-10-25T15:50:43.572211573+01:00","Action":"output","Package":"example.com/racemain","Output":"Goroutine 9 (running) created at:\n"} +{"Time":"2026-10-25T15:50:43.572214803+01:00","Action":"output","Package":"example.com/racemain","Output":" example.com/racemain.TestMain()\n"} +{"Time":"2026-10-25T15:50:43.572218043+01:00","Action":"output","Package":"example.com/racemain","Output":" /tmp/fix-1901b/racemain/p_test.go:15 +0x37\n"} +{"Time":"2026-10-25T15:50:43.572225414+01:00","Action":"output","Package":"example.com/racemain","Output":" main.main()\n"} +{"Time":"2026-10-25T15:50:43.572228654+01:00","Action":"output","Package":"example.com/racemain","Output":" _testmain.go:48 +0x171\n"} +{"Time":"2026-10-25T15:50:43.572231664+01:00","Action":"output","Package":"example.com/racemain","Output":"==================\n"} +{"Time":"2026-10-25T15:50:44.581262249+01:00","Action":"output","Package":"example.com/racemain","Output":"Found 1 data race(s)\n"} +{"Time":"2026-10-25T15:50:44.584693204+01:00","Action":"output","Package":"example.com/racemain","Output":"FAIL\texample.com/racemain\t1.031s\n","OutputType":"frame"} +{"Time":"2026-10-25T15:50:44.584737425+01:00","Action":"fail","Package":"example.com/racemain","Elapsed":1.033} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/subpanic.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/subpanic.jsonl new file mode 100644 index 00000000..ec85af78 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/subpanic.jsonl @@ -0,0 +1,30 @@ +{"Time":"2026-10-26T20:15:37.791161045+01:00","Action":"start","Package":"example.com/subpanic"} +{"Time":"2026-10-26T20:15:37.793205666+01:00","Action":"run","Package":"example.com/subpanic","Test":"TestTable"} +{"Time":"2026-10-26T20:15:37.793257158+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"=== RUN TestTable\n","OutputType":"frame"} +{"Time":"2026-10-26T20:15:37.793288149+01:00","Action":"run","Package":"example.com/subpanic","Test":"TestTable/passes"} +{"Time":"2026-10-26T20:15:37.793290669+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable/passes","Output":"=== RUN TestTable/passes\n","OutputType":"frame"} +{"Time":"2026-10-26T20:15:37.793298009+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable/passes","Output":"--- PASS: TestTable/passes (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-26T20:15:37.793311679+01:00","Action":"pass","Package":"example.com/subpanic","Test":"TestTable/passes","Elapsed":0} +{"Time":"2026-10-26T20:15:37.79331977+01:00","Action":"run","Package":"example.com/subpanic","Test":"TestTable/panics"} +{"Time":"2026-10-26T20:15:37.79332377+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable/panics","Output":"=== RUN TestTable/panics\n","OutputType":"frame"} +{"Time":"2026-10-26T20:15:37.79332773+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable/panics","Output":"--- FAIL: TestTable/panics (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-26T20:15:37.79333111+01:00","Action":"fail","Package":"example.com/subpanic","Test":"TestTable/panics","Elapsed":0} +{"Time":"2026-10-26T20:15:37.79333383+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"--- FAIL: TestTable (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-26T20:15:37.795322529+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"panic: assignment to entry in nil map [recovered, repanicked]\n"} +{"Time":"2026-10-26T20:15:37.795331489+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"\n"} +{"Time":"2026-10-26T20:15:37.795340579+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"goroutine 9 [running]:\n"} +{"Time":"2026-10-26T20:15:37.79534387+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"testing.tRunner.func1.2({0x6b7010, 0x6eef80})\n"} +{"Time":"2026-10-26T20:15:37.79534657+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"\t/usr/lib/go/src/testing/testing.go:2125 +0x232\n"} +{"Time":"2026-10-26T20:15:37.79534907+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"testing.tRunner.func1()\n"} +{"Time":"2026-10-26T20:15:37.79535172+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"\t/usr/lib/go/src/testing/testing.go:2128 +0x329\n"} +{"Time":"2026-10-26T20:15:37.79535625+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"panic({0x6b7010?, 0x6eef80?})\n"} +{"Time":"2026-10-26T20:15:37.79535879+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"\t/usr/lib/go/src/runtime/panic.go:859 +0x125\n"} +{"Time":"2026-10-26T20:15:37.79536129+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"example.com/subpanic.TestTable.func2(0x3b39436666c8?)\n"} +{"Time":"2026-10-26T20:15:37.79536452+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"\t/tmp/1901k/go/subpanic/p_test.go:7 +0x28\n"} +{"Time":"2026-10-26T20:15:37.79536782+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"testing.tRunner(0x3b39436666c8, 0x6d4b58)\n"} +{"Time":"2026-10-26T20:15:37.7953704+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"\t/usr/lib/go/src/testing/testing.go:2195 +0xea\n"} +{"Time":"2026-10-26T20:15:37.79537289+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"created by testing.(*T).Run in goroutine 7\n"} +{"Time":"2026-10-26T20:15:37.795381681+01:00","Action":"output","Package":"example.com/subpanic","Test":"TestTable","Output":"\t/usr/lib/go/src/testing/testing.go:2260 +0x4d4\n"} +{"Time":"2026-10-26T20:15:37.795731441+01:00","Action":"fail","Package":"example.com/subpanic","Test":"TestTable","Elapsed":0} +{"Time":"2026-10-26T20:15:37.795759332+01:00","Action":"output","Package":"example.com/subpanic","Output":"FAIL\texample.com/subpanic\t0.004s\n","OutputType":"frame"} +{"Time":"2026-10-26T20:15:37.795783503+01:00","Action":"fail","Package":"example.com/subpanic","Elapsed":0.005} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/subtests.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/subtests.jsonl new file mode 100644 index 00000000..b12cb5ac --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/subtests.jsonl @@ -0,0 +1,17 @@ +{"Time":"2026-10-26T19:40:29.768101399+01:00","Action":"start","Package":"example.com/subtests"} +{"Time":"2026-10-26T19:40:29.770109733+01:00","Action":"run","Package":"example.com/subtests","Test":"TestTheTable"} +{"Time":"2026-10-26T19:40:29.770152272+01:00","Action":"output","Package":"example.com/subtests","Test":"TestTheTable","Output":"=== RUN TestTheTable\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.770189372+01:00","Action":"run","Package":"example.com/subtests","Test":"TestTheTable/passes"} +{"Time":"2026-10-26T19:40:29.770191882+01:00","Action":"output","Package":"example.com/subtests","Test":"TestTheTable/passes","Output":"=== RUN TestTheTable/passes\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.770197442+01:00","Action":"output","Package":"example.com/subtests","Test":"TestTheTable/passes","Output":"--- PASS: TestTheTable/passes (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.770204352+01:00","Action":"pass","Package":"example.com/subtests","Test":"TestTheTable/passes","Elapsed":0} +{"Time":"2026-10-26T19:40:29.770209622+01:00","Action":"run","Package":"example.com/subtests","Test":"TestTheTable/fails"} +{"Time":"2026-10-26T19:40:29.770211832+01:00","Action":"output","Package":"example.com/subtests","Test":"TestTheTable/fails","Output":"=== RUN TestTheTable/fails\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.770214722+01:00","Action":"output","Package":"example.com/subtests","Test":"TestTheTable/fails","Output":" s_test.go:7: got 3 machines, want 4\n","OutputType":"error"} +{"Time":"2026-10-26T19:40:29.770218032+01:00","Action":"output","Package":"example.com/subtests","Test":"TestTheTable/fails","Output":"--- FAIL: TestTheTable/fails (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.770220732+01:00","Action":"fail","Package":"example.com/subtests","Test":"TestTheTable/fails","Elapsed":0} +{"Time":"2026-10-26T19:40:29.770223522+01:00","Action":"output","Package":"example.com/subtests","Test":"TestTheTable","Output":"--- FAIL: TestTheTable (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.770226292+01:00","Action":"fail","Package":"example.com/subtests","Test":"TestTheTable","Elapsed":0} +{"Time":"2026-10-26T19:40:29.770228582+01:00","Action":"output","Package":"example.com/subtests","Output":"FAIL\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.770561979+01:00","Action":"output","Package":"example.com/subtests","Output":"FAIL\texample.com/subtests\t0.002s\n","OutputType":"frame"} +{"Time":"2026-10-26T19:40:29.770581629+01:00","Action":"fail","Package":"example.com/subtests","Elapsed":0.002} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/go/timeout.jsonl b/modules/mesh-build-go/buildstep/conformance/captured/go/timeout.jsonl new file mode 100644 index 00000000..12a2c12f --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/go/timeout.jsonl @@ -0,0 +1,43 @@ +{"Time":"2026-10-25T15:34:36.049580618+01:00","Action":"start","Package":"example.com/timeout"} +{"Time":"2026-10-25T15:34:36.053250986+01:00","Action":"run","Package":"example.com/timeout","Test":"TestQuick"} +{"Time":"2026-10-25T15:34:36.053354018+01:00","Action":"output","Package":"example.com/timeout","Test":"TestQuick","Output":"=== RUN TestQuick\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:36.053459789+01:00","Action":"output","Package":"example.com/timeout","Test":"TestQuick","Output":"--- PASS: TestQuick (0.00s)\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:36.05348499+01:00","Action":"pass","Package":"example.com/timeout","Test":"TestQuick","Elapsed":0} +{"Time":"2026-10-25T15:34:36.0535114+01:00","Action":"run","Package":"example.com/timeout","Test":"TestWaitsForever"} +{"Time":"2026-10-25T15:34:36.05351549+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"=== RUN TestWaitsForever\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:36.053567451+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":" p_test.go:11: waiting for the bus\n"} +{"Time":"2026-10-25T15:34:38.064693307+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"panic: test timed out after 2s\n"} +{"Time":"2026-10-25T15:34:38.06487122+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\trunning tests:\n"} +{"Time":"2026-10-25T15:34:38.06491684+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t\tTestWaitsForever (2s)\n"} +{"Time":"2026-10-25T15:34:38.065034382+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\n"} +{"Time":"2026-10-25T15:34:38.065108573+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"goroutine 33 [running]:\n"} +{"Time":"2026-10-25T15:34:38.065154754+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"testing.(*M).startAlarm.func1()\n"} +{"Time":"2026-10-25T15:34:38.065386208+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/testing/testing.go:2961 +0x34a\n"} +{"Time":"2026-10-25T15:34:38.065431878+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"created by time.goFunc\n"} +{"Time":"2026-10-25T15:34:38.0655151+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/time/sleep.go:182 +0x2d\n"} +{"Time":"2026-10-25T15:34:38.06553471+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\n"} +{"Time":"2026-10-25T15:34:38.065847805+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"goroutine 1 [chan receive]:\n"} +{"Time":"2026-10-25T15:34:38.066038298+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"testing.(*T).Run(0x3a0209bc8008, {0x5580aa?, 0x3a0209a9caa0?}, 0x6d5b10)\n"} +{"Time":"2026-10-25T15:34:38.066047488+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/testing/testing.go:2268 +0x4f2\n"} +{"Time":"2026-10-25T15:34:38.066051368+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"testing.runTests.func1(0x3a0209bc8008)\n"} +{"Time":"2026-10-25T15:34:38.066056128+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/testing/testing.go:2744 +0x37\n"} +{"Time":"2026-10-25T15:34:38.066059478+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"testing.tRunner(0x3a0209bc8008, 0x3a0209a9cbc8)\n"} +{"Time":"2026-10-25T15:34:38.066064958+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/testing/testing.go:2195 +0xea\n"} +{"Time":"2026-10-25T15:34:38.066069599+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"testing.runTests({0x559017, 0x13}, {0x559017, 0x13}, 0x3a0209b8e0d8, {0x6f19f0, 0x2, 0x2}, {0xc2afa2bf832a3f56, 0x773908b5, ...})\n"} +{"Time":"2026-10-25T15:34:38.066083309+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/testing/testing.go:2742 +0x510\n"} +{"Time":"2026-10-25T15:34:38.066086879+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"testing.(*M).Run(0x3a0209b8c1e0)\n"} +{"Time":"2026-10-25T15:34:38.066090269+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/testing/testing.go:2602 +0x6af\n"} +{"Time":"2026-10-25T15:34:38.066093429+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"main.main()\n"} +{"Time":"2026-10-25T15:34:38.066097569+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t_testmain.go:48 +0x9b\n"} +{"Time":"2026-10-25T15:34:38.066100639+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\n"} +{"Time":"2026-10-25T15:34:38.066103769+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"goroutine 20 [sleep]:\n"} +{"Time":"2026-10-25T15:34:38.066106969+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"time.Sleep(0xdf8475800)\n"} +{"Time":"2026-10-25T15:34:38.066110279+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/runtime/time.go:368 +0x165\n"} +{"Time":"2026-10-25T15:34:38.066113639+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"example.com/timeout.TestWaitsForever(0x3a0209bc8488?)\n"} +{"Time":"2026-10-25T15:34:38.066117109+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/tmp/fix-1901b/timeout/p_test.go:12 +0x48\n"} +{"Time":"2026-10-25T15:34:38.066120239+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"testing.tRunner(0x3a0209bc8488, 0x6d5b10)\n"} +{"Time":"2026-10-25T15:34:38.066123509+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/testing/testing.go:2195 +0xea\n"} +{"Time":"2026-10-25T15:34:38.066126839+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"created by testing.(*T).Run in goroutine 1\n"} +{"Time":"2026-10-25T15:34:38.066129989+01:00","Action":"output","Package":"example.com/timeout","Test":"TestWaitsForever","Output":"\t/usr/lib/go/src/testing/testing.go:2260 +0x4d4\n"} +{"Time":"2026-10-25T15:34:38.066986453+01:00","Action":"output","Package":"example.com/timeout","Output":"FAIL\texample.com/timeout\t2.017s\n","OutputType":"frame"} +{"Time":"2026-10-25T15:34:38.067002113+01:00","Action":"fail","Package":"example.com/timeout","Elapsed":2.017} diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/crash.console b/modules/mesh-build-go/buildstep/conformance/captured/node/crash.console new file mode 100644 index 00000000..90a8ef65 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/crash.console @@ -0,0 +1,2 @@ +npm notice run test +npm notice run node --test --experimental-strip-types 'test/*.test.ts' diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/crash.tap b/modules/mesh-build-go/buildstep/conformance/captured/node/crash.tap new file mode 100644 index 00000000..bea3c651 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/crash.tap @@ -0,0 +1,41 @@ +TAP version 13 +# Subtest: passes before the crash +ok 1 - passes before the crash + --- + duration_ms: 0.746694 + type: 'test' + ... +# Subtest: throws after it returned +ok 2 - throws after it returned + --- + duration_ms: 0.763364 + type: 'test' + ... +# Subtest: waits after the throw +ok 3 - waits after the throw + --- + duration_ms: 49.953893 + type: 'test' + ... +# Error: Test "throws after it returned" at test/b.test.ts:2:1 generated asynchronous activity after the test ended. This activity created the error "Error: the bus closed under the test" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Subtest: test/b.test.ts +not ok 2 - test/b.test.ts + --- + duration_ms: 155.864774 + type: 'test' + location: '/tmp/1901k/node/crash/test/b.test.ts:1:1' + failureType: 'testCodeFailure' + exitCode: 1 + signal: ~ + error: 'test failed' + code: 'ERR_TEST_FAILURE' + ... +1..4 +# tests 4 +# suites 0 +# pass 3 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 168.479173 diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/logcrash.console b/modules/mesh-build-go/buildstep/conformance/captured/node/logcrash.console new file mode 100644 index 00000000..90a8ef65 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/logcrash.console @@ -0,0 +1,2 @@ +npm notice run test +npm notice run node --test --experimental-strip-types 'test/*.test.ts' diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/logcrash.tap b/modules/mesh-build-go/buildstep/conformance/captured/node/logcrash.tap new file mode 100644 index 00000000..89dbfb8f --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/logcrash.tap @@ -0,0 +1,42 @@ +TAP version 13 +# retrying after Error: the store was not ready yet +# Subtest: passes and logs +ok 1 - passes and logs + --- + duration_ms: 1.184595 + type: 'test' + ... +# Subtest: throws after it returned +ok 2 - throws after it returned + --- + duration_ms: 0.748862 + type: 'test' + ... +# Subtest: waits after the throw +ok 3 - waits after the throw + --- + duration_ms: 51.019875 + type: 'test' + ... +# Error: Test "throws after it returned" at test/b.test.ts:2:1 generated asynchronous activity after the test ended. This activity created the error "Error: the bus closed under the test" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Subtest: test/b.test.ts +not ok 2 - test/b.test.ts + --- + duration_ms: 172.91715 + type: 'test' + location: '/tmp/1901k/node/logcrash/test/b.test.ts:1:1' + failureType: 'testCodeFailure' + exitCode: 1 + signal: ~ + error: 'test failed' + code: 'ERR_TEST_FAILURE' + ... +1..4 +# tests 4 +# suites 0 +# pass 3 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 185.744334 diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/suite.console b/modules/mesh-build-go/buildstep/conformance/captured/node/suite.console new file mode 100644 index 00000000..90a8ef65 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/suite.console @@ -0,0 +1,2 @@ +npm notice run test +npm notice run node --test --experimental-strip-types 'test/*.test.ts' diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/suite.tap b/modules/mesh-build-go/buildstep/conformance/captured/node/suite.tap new file mode 100644 index 00000000..06e97611 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/suite.tap @@ -0,0 +1,61 @@ +TAP version 13 +# Subtest: the seat + # Subtest: passes inside + ok 1 - passes inside + --- + duration_ms: 0.577396 + type: 'test' + ... + # Subtest: fails inside + not ok 2 - fails inside + --- + duration_ms: 1.621198 + type: 'test' + location: '/tmp/1901k/node/suite/test/a.test.ts:5:3' + failureType: 'testCodeFailure' + error: |- + Expected values to be strictly deep-equal: + + actual - expected + +  { + + holders: 2 + - holders: 1 +  } + + code: 'ERR_ASSERTION' + name: 'AssertionError' + expected: + holders: 1 + actual: + holders: 2 + operator: 'deepStrictEqual' + stack: |- + TestContext. (file:///tmp/1901k/node/suite/test/a.test.ts:5:37) + Test.runInAsyncScope (node:async_hooks:226:14) + Test.run (node:internal/test_runner/test:1402:25) + Suite.processPendingSubtests (node:internal/test_runner/test:974:18) + Test.postRun (node:internal/test_runner/test:1542:19) + Test.run (node:internal/test_runner/test:1467:12) + async Promise.all (index 0) + async Suite.run (node:internal/test_runner/test:1905:7) + async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) + ... + 1..2 +not ok 1 - the seat + --- + duration_ms: 3.295664 + type: 'suite' + location: '/tmp/1901k/node/suite/test/a.test.ts:3:1' + failureType: 'subtestsFailed' + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + ... +1..1 +# tests 2 +# suites 1 +# pass 1 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 115.948112 diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/syntax.console b/modules/mesh-build-go/buildstep/conformance/captured/node/syntax.console new file mode 100644 index 00000000..90a8ef65 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/syntax.console @@ -0,0 +1,2 @@ +npm notice run test +npm notice run node --test --experimental-strip-types 'test/*.test.ts' diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/syntax.tap b/modules/mesh-build-go/buildstep/conformance/captured/node/syntax.tap new file mode 100644 index 00000000..ceadd12c --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/syntax.tap @@ -0,0 +1,47 @@ +TAP version 13 +# Subtest: passes in the first file +ok 1 - passes in the first file + --- + duration_ms: 0.637465 + type: 'test' + ... +# node:internal/modules/run_main:111 +# triggerUncaughtException( +# ^ +# file:///tmp/1901k/node/syntax/test/b.test.ts:2 +# test('never loads', () => { +# SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]: Expected ',', got '' +#  at parseTypeScript (node:internal/modules/typescript:61:36) +#  at processTypeScriptCode (node:internal/modules/typescript:129:37) +#  at Object.stripTypeScriptModuleTypes (node:internal/modules/typescript:172:22) +#  at stripTypeScriptModuleTypes (node:internal/modules/esm/translators:35:49) +#  at ModuleLoader. (node:internal/modules/esm/translators:688:29) +#  at \#translate (node:internal/modules/esm/loader:441:20) +#  at afterLoad (node:internal/modules/esm/loader:509:29) +#  at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:514:12) +#  at \#getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:567:36) +#  at afterResolve (node:internal/modules/esm/loader:614:52) { +# code: 'ERR_INVALID_TYPESCRIPT_SYNTAX' +# } +# Node.js v26.12.1 +# Subtest: test/b.test.ts +not ok 2 - test/b.test.ts + --- + duration_ms: 87.629815 + type: 'test' + location: '/tmp/1901k/node/syntax/test/b.test.ts:1:1' + failureType: 'testCodeFailure' + exitCode: 1 + signal: ~ + error: 'test failed' + code: 'ERR_TEST_FAILURE' + ... +1..2 +# tests 2 +# suites 0 +# pass 1 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 106.551435 diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/timeout.console b/modules/mesh-build-go/buildstep/conformance/captured/node/timeout.console new file mode 100644 index 00000000..90a8ef65 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/timeout.console @@ -0,0 +1,2 @@ +npm notice run test +npm notice run node --test --experimental-strip-types 'test/*.test.ts' diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/timeout.tap b/modules/mesh-build-go/buildstep/conformance/captured/node/timeout.tap new file mode 100644 index 00000000..dd87e171 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/timeout.tap @@ -0,0 +1,26 @@ +TAP version 13 +# Subtest: passes before the wait +ok 1 - passes before the wait + --- + duration_ms: 0.683015 + type: 'test' + ... +# Subtest: waits for an answer that never comes +not ok 2 - waits for an answer that never comes + --- + duration_ms: 201.103775 + type: 'test' + location: '/tmp/1901k/node/timeout/test/a.test.ts:3:1' + failureType: 'testTimeoutFailure' + error: 'test timed out after 200ms' + code: 'ERR_TEST_FAILURE' + ... +1..2 +# tests 2 +# suites 0 +# pass 1 +# fail 0 +# cancelled 1 +# skipped 0 +# todo 0 +# duration_ms 308.920061 diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/two-files.console b/modules/mesh-build-go/buildstep/conformance/captured/node/two-files.console new file mode 100644 index 00000000..90a8ef65 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/two-files.console @@ -0,0 +1,2 @@ +npm notice run test +npm notice run node --test --experimental-strip-types 'test/*.test.ts' diff --git a/modules/mesh-build-go/buildstep/conformance/captured/node/two-files.tap b/modules/mesh-build-go/buildstep/conformance/captured/node/two-files.tap new file mode 100644 index 00000000..8e0a85b8 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/node/two-files.tap @@ -0,0 +1,49 @@ +TAP version 13 +# Subtest: passes in the first file +ok 1 - passes in the first file + --- + duration_ms: 0.740124 + type: 'test' + ... +# judged against the seat as it stands +# Subtest: passes in the second file first +ok 2 - passes in the second file first + --- + duration_ms: 0.916463 + type: 'test' + ... +# Subtest: fails in the second file +not ok 3 - fails in the second file + --- + duration_ms: 1.719236 + type: 'test' + location: '/tmp/1901k/node/two-files/test/b.test.ts:4:1' + failureType: 'testCodeFailure' + error: |- + one plus one is not three + + 2 !== 3 + + code: 'ERR_ASSERTION' + name: 'AssertionError' + expected: 3 + actual: 2 + operator: 'strictEqual' + stack: |- + TestContext. (file:///tmp/1901k/node/two-files/test/b.test.ts:6:10) + Test.runInAsyncScope (node:async_hooks:226:14) + Test.run (node:internal/test_runner/test:1402:25) + Test.processPendingSubtests (node:internal/test_runner/test:974:18) + Test.postRun (node:internal/test_runner/test:1542:19) + Test.run (node:internal/test_runner/test:1467:12) + async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) + ... +1..3 +# tests 3 +# suites 0 +# pass 2 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 135.931121 diff --git a/modules/mesh-build-go/buildstep/conformance/captured/tsc/typeerror.console b/modules/mesh-build-go/buildstep/conformance/captured/tsc/typeerror.console new file mode 100644 index 00000000..ac7713ed --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/captured/tsc/typeerror.console @@ -0,0 +1 @@ +seat.ts(2,14): error TS2322: Type 'number' is not assignable to type 'string'. diff --git a/modules/mesh-build-go/buildstep/conformance/conformance.go b/modules/mesh-build-go/buildstep/conformance/conformance.go new file mode 100644 index 00000000..78596ff2 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/conformance.go @@ -0,0 +1,201 @@ +// Package conformance is the kit every build module's tests run (novox/hq issue 1901, its thorough fix): real output of +// the test runners, captured with the build nodes' own toolchains (captured/README.md), and what a build module must say +// of each failure in it under the failure contract (buildstep.Failure). A build module's test calls Run with its own parse +// path, the code that turns a runner's output into the failures of its test result; the kit fails the test when a +// failure is not named, its cause is missing or wrong, or a passing line stands in its place. +// +// A catalogue check (mesh-catalog checks/failurekit) refuses a build module with a test step whose tests do not call Run, +// and any build module whose tests do not call LeftRunning, the kit's case of steps a previous process left running +// (leftrunning.go, issue 1890). +package conformance + +import ( + "embed" + "fmt" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/buildstep" +) + +// The runners the kit holds output of. +const ( + // GoTest is `go test -json -count=1 ./...`: Output.Stdout is its output (go1.27.3). + GoTest = "go test -json" + // NodeTest is a package's `npm test` running `node --test --experimental-strip-types` with + // NODE_OPTIONS=buildstep.NodeTestOptions(): Output.Report is the TAP file, Output.Stdout the script's own output + // and stderr, Output.Dir the package's directory as it was captured (node v26.12.1, npm 12.2.1). + NodeTest = "node --test" + // TypeCheck is tsc's type-check (`tsc --noEmit`, typescript 5.9.3): Output.Stdout is what it wrote; its failure + // names the test "type-check". + TypeCheck = "tsc" +) + +// Output is one captured run of a runner, as a build module's parse path takes it. +type Output struct { + Stdout, Stderr string + // Report is what a runner wrote to a report file of its own (node --test's TAP), "" for the others. + Report string + // Dir is the directory the run was captured in: a node test's file is said relative to it. + Dir string +} + +// Parse is a build module's own parse path: the failures its test result would carry for one run that failed. +type Parse func(Output) []buildstep.Failure + +// Case is one captured run and what its failure must say. +type Case struct { + Name string + Runner string + // Test is the failing test the failure names (for a package that stopped, the tests that were running; "" for none). + Test string + // File, when set, is where the failing test is. + File string + // Cause holds what the failure's cause must contain. + Cause []string + // Passing are the names of tests that passed in the run, and what they logged: no failure's words may hold them. + Passing []string +} + +//go:embed captured +var captured embed.FS + +// Cases are the kit's cases of a runner. +func Cases(runner string) []Case { + var out []Case + for _, c := range cases { + if c.Runner == runner { + out = append(out, c) + } + } + return out +} + +var cases = []Case{ + {Name: "passfirst", Runner: GoTest, Test: "TestTheSecondPackageFails", Cause: []string{"the seat has 2 holders, want 1"}, + Passing: []string{"TestTheFirstPackagePasses", "TestTheSecondPackagePassesFirst"}}, + {Name: "subtests", Runner: GoTest, Test: "TestTheTable/fails", Cause: []string{"got 3 machines, want 4"}, Passing: []string{"TestTheTable/passes"}}, + {Name: "logthenfatal", Runner: GoTest, Test: "TestThree", Cause: []string{"3 subscription(s) derive to a subject no emitter owns:"}}, + {Name: "panicking", Runner: GoTest, Test: "TestPanics", Cause: []string{"panic: assignment to entry in nil map"}}, + {Name: "timeout", Runner: GoTest, Test: "TestWaitsForever", Cause: []string{"panic: test timed out after 2s", "TestWaitsForever"}}, + {Name: "deeptimeout", Runner: GoTest, Test: "TestHoldsTheSeat, TestWaitsForTheBus", Cause: []string{"panic: test timed out after 2s", "TestHoldsTheSeat", "TestWaitsForTheBus"}}, + {Name: "buildfail", Runner: GoTest, Cause: []string{`cannot use "seat" (untyped string constant) as int value`}}, + {Name: "exiting", Runner: GoTest, Test: "TestExits", Cause: []string{"exited", "TestExits"}}, + {Name: "killed", Runner: GoTest, Test: "TestKilled", Cause: []string{"signal: killed"}}, + {Name: "passthenfail", Runner: GoTest, Cause: []string{"panic: the bus closed after the tests: nats: connection closed"}}, + {Name: "mainexit", Runner: GoTest, Cause: []string{"passed", "exited"}}, + {Name: "race", Runner: GoTest, Test: "TestRacesOnTheSeat", Cause: []string{"DATA RACE", "Write at", "example.com/race.TestRacesOnTheSeat.func1()", "p_test.go:13", "created at example.com/race.TestRacesOnTheSeat()"}}, + {Name: "subpanic", Runner: GoTest, Test: "TestTable/panics", Cause: []string{"panic: assignment to entry in nil map"}, Passing: []string{"TestTable/passes"}}, + {Name: "racemain", Runner: GoTest, Cause: []string{"DATA RACE", "Write at", "example.com/racemain.TestMain.func1()", "p_test.go:15", "created at example.com/racemain.TestMain()"}}, + + {Name: "two-files", Runner: NodeTest, Test: "fails in the second file", File: "test/b.test.ts:4:1", Cause: []string{"one plus one is not three"}, + Passing: []string{"passes in the first file", "passes in the second file first"}}, + {Name: "suite", Runner: NodeTest, Test: "fails inside", File: "test/a.test.ts:5:3", Cause: []string{"Expected values to be strictly deep-equal:"}, + Passing: []string{"passes inside"}}, + {Name: "crash", Runner: NodeTest, Test: "test/b.test.ts", File: "test/b.test.ts:1:1", Cause: []string{"the bus closed under the test"}, + Passing: []string{"passes before the crash"}}, + {Name: "logcrash", Runner: NodeTest, Test: "test/b.test.ts", File: "test/b.test.ts:1:1", Cause: []string{"the bus closed under the test"}, + Passing: []string{"passes and logs", "retrying after Error: the store was not ready yet"}}, + {Name: "timeout", Runner: NodeTest, Test: "waits for an answer that never comes", File: "test/a.test.ts:3:1", Cause: []string{"test timed out after 200ms"}, + Passing: []string{"passes before the wait"}}, + {Name: "syntax", Runner: NodeTest, Test: "test/b.test.ts", File: "test/b.test.ts:1:1", Cause: []string{"SyntaxError", "Expected ',', got ''"}, + Passing: []string{"passes in the first file"}}, + + {Name: "typeerror", Runner: TypeCheck, Test: "type-check", Cause: []string{"seat.ts(2,14): error TS2322: Type 'number' is not assignable to type 'string'."}}, +} + +// Load is the captured output of a case. +func Load(c Case) (Output, error) { + switch c.Runner { + case GoTest: + b, err := captured.ReadFile("captured/go/" + c.Name + ".jsonl") + return Output{Stdout: string(b)}, err + case NodeTest: + report, err := captured.ReadFile("captured/node/" + c.Name + ".tap") + if err != nil { + return Output{}, err + } + console, err := captured.ReadFile("captured/node/" + c.Name + ".console") + return Output{Report: string(report), Stdout: string(console), Dir: "/tmp/1901k/node/" + c.Name}, err + default: + b, err := captured.ReadFile("captured/tsc/" + c.Name + ".console") + return Output{Stdout: string(b), Dir: "/tmp/1901k/tsc/" + c.Name}, err + } +} + +// Run runs every case of runner through parse, each as a subtest, and fails a case whose failure does not keep the +// contract: exactly one failure, naming the failing test (and its file, where the runner says it), with a cause that +// holds what the case says and is no passing line, an output that starts with the cause, stays within its bound and +// holds no passing line and no passing test's name. +func Run(t *testing.T, runner string, parse Parse) { + t.Helper() + cs := Cases(runner) + if len(cs) == 0 { + t.Fatalf("the kit holds no case of the runner %q", runner) + } + for _, c := range cs { + t.Run(c.Name, func(t *testing.T) { + out, err := Load(c) + if err != nil { + t.Fatal(err) + } + for _, problem := range Judge(c, parse(out)) { + t.Error(problem) + } + }) + } +} + +// Judge says what is wrong with the failures a parse path made of a case; nothing when they keep the contract. +func Judge(c Case, failed []buildstep.Failure) []string { + var problems []string + say := func(s string) { problems = append(problems, c.Runner+" "+c.Name+": "+s) } + if len(failed) != 1 { + say(fmt.Sprintf("want one failure, got %d: %s", len(failed), describe(failed))) + if len(failed) == 0 { + return problems + } + } + f := failed[0] + if f.Test != c.Test { + say("the failure names the test " + quote(f.Test) + ", want " + quote(c.Test)) + } + if c.File != "" && f.File != c.File { + say("the failure's file is " + quote(f.File) + ", want " + quote(c.File)) + } + if why := buildstep.Hidden(f); why != "" { + say("the failure hides its cause: " + why) + } + for _, want := range c.Cause { + if !strings.Contains(f.Cause, want) { + say("the cause " + quote(f.Cause) + " does not hold " + quote(want)) + } + } + if first := strings.SplitN(f.Output, "\n", 2)[0]; strings.TrimSpace(first) != strings.TrimSpace(f.Cause) { + say("the output starts with " + quote(first) + ", not with the cause") + } + if len(f.Output) > buildstep.OutputBytes+buildstep.LineBytes+64 { + say(fmt.Sprintf("the output holds %d bytes, over its bound", len(f.Output))) + } + for _, l := range strings.Split(f.Output, "\n") { + if buildstep.PassingLine(l) { + say("the output keeps the passing line " + quote(l)) + } + } + for _, p := range c.Passing { + if strings.Contains(f.Output, p+"\n") || strings.HasSuffix(f.Output, p) || strings.Contains(f.Cause, p) || strings.Contains(f.Output, "- "+p) { + say("the failure's words hold the passing test " + quote(p)) + } + } + return problems +} + +func describe(failed []buildstep.Failure) string { + var s []string + for _, f := range failed { + s = append(s, quote(f.Package+" "+f.Test)+" ("+quote(f.Cause)+")") + } + return strings.Join(s, ", ") +} + +func quote(s string) string { return fmt.Sprintf("%q", s) } diff --git a/modules/mesh-build-go/buildstep/conformance/conformance_test.go b/modules/mesh-build-go/buildstep/conformance/conformance_test.go new file mode 100644 index 00000000..0e1745d9 --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/conformance_test.go @@ -0,0 +1,168 @@ +package conformance + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" + sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep" +) + +// The failure contract (issue 1901): the SDK's own parsers keep it on every captured case of the kit (1901/AC6); the +// kit refuses what the build modules sent before it; the contract's own rules; and the Go reader's older guarantees. +func TestTheFailureContract(t *testing.T) { + t.Run("the SDK parsers keep it", func(t *testing.T) { + Run(t, GoTest, func(o Output) []sdkstep.Failure { return buildstep.ReadGoTest(o.Stdout, ".").Failed }) + Run(t, NodeTest, func(o Output) []sdkstep.Failure { return buildstep.ReadTAP(".", "test", o.Dir, o.Report, o.Stdout) }) + Run(t, TypeCheck, func(o Output) []sdkstep.Failure { + return []sdkstep.Failure{sdkstep.ConsoleFailure(".", "type-check", o.Stdout+o.Stderr)} + }) + }) + + // 1890/AC7: the SDK's start sweep keeps the left-running case, and the kit refuses a sweep that ends nothing or ends + // everything. + t.Run("the left-running case", func(t *testing.T) { + LeftRunning(t, func(ctx context.Context, scan sdkstep.Scan, node string, before time.Time, logf func(string, ...any)) { + sdkstep.EndStaleAtStart(ctx, scan, node, before, logf) + }) + if p := JudgeLeftRunning(func(context.Context, sdkstep.Scan, string, time.Time, func(string, ...any)) {}); len(p) != 11 { + t.Errorf("a sweep that ends nothing: %d problems, want 11 (three steps not ended and unsaid, one owner unnamed, two asks not deleted and unsaid):\n%s", len(p), strings.Join(p, "\n")) + } + wrongNode := func(ctx context.Context, scan sdkstep.Scan, _ string, before time.Time, logf func(string, ...any)) { + sdkstep.EndStaleAtStart(ctx, scan, "kitother", before, logf) + } + if p := JudgeLeftRunning(wrongNode); len(p) == 0 { + t.Error("a sweep of another node's steps is not refused") + } + }) + + // node's output cut to its first fifteen lines with no cause, whose first line was a passing test of the first file + // (mesh-build-node's lines() before the contract), and a Go failure keeping its first line. + t.Run("the kit refuses a failure that hides its cause", func(t *testing.T) { + firstLines := func(s string) string { + var keep []string + for _, l := range strings.Split(s, "\n") { + if l = strings.TrimSpace(l); l != "" && len(keep) < 15 { + keep = append(keep, l) + } + } + return strings.Join(keep, "\n") + } + for _, c := range []struct { + runner, name string + old func(Output) []sdkstep.Failure + }{ + {NodeTest, "two-files", func(o Output) []sdkstep.Failure { + return []sdkstep.Failure{{Package: ".", Test: "test", Output: firstLines(o.Report)}} + }}, + {GoTest, "passfirst", func(o Output) []sdkstep.Failure { + return []sdkstep.Failure{{Package: "example.com/passfirst/b", Test: "TestTheSecondPackageFails", Cause: "ok \texample.com/passfirst/a", Output: "--- PASS: TestTheSecondPackagePassesFirst"}} + }}, + } { + for _, k := range Cases(c.runner) { + if k.Name != c.name { + continue + } + out, err := Load(k) + if err != nil { + t.Fatal(err) + } + if problems := Judge(k, c.old(out)); len(problems) == 0 { + t.Errorf("%s %s: the kit passed a failure that hides its cause", c.runner, c.name) + } + } + } + }) + + // What a passing line is (a test's own line starting "ok " is none), when a failure hides its cause, and that a long + // failure keeps its last lines within the bound and starts with a real line for an older checker, which reads only + // the first line of the output cut to 120 characters (1901/AC1, AC3). + t.Run("its rules", func(t *testing.T) { + for _, c := range []struct { + line string + passing bool + }{ + {"✔ the same merge has the same id (8.0ms)", true}, {"ok 1 - passes", true}, {" ok 3 - inside", true}, + {"ok \texample.com/a\t0.01s", true}, {"PASS", true}, {"--- PASS: TestA (0.00s)", true}, + {" --- PASS: TestA/sub (0.00s)", true}, {"✖ fails (1ms)", false}, {"not ok 2 - fails", false}, + {"okay, the seat moved", false}, {"ok then: the store refused the write", false}, {"ok the seat", false}, {"FAIL", false}, + } { + if sdkstep.PassingLine(c.line) != c.passing { + t.Errorf("PassingLine(%q) = %v, want %v", c.line, !c.passing, c.passing) + } + } + if sdkstep.Hidden(sdkstep.Failure{Cause: "the seat has 2 holders"}) != "" || sdkstep.Hidden(sdkstep.Failure{}) == "" || + sdkstep.Hidden(sdkstep.Failure{Cause: "✔ passes (1ms)"}) == "" { + t.Error("Hidden does not tell a real cause from none and from a passing line") + } + var lines []string + for i := 0; i < 400; i++ { + lines = append(lines, " x_test.go:1: log line "+strings.Repeat("y", 40)) + } + lines = append(lines, " x_test.go:9: the real cause", "--- FAIL: TestX (0.00s)") + oldFirst := func(out string) string { // the older checker's rule (judge.go firstLine before issue 1901) + for _, l := range strings.Split(out, "\n") { + if l = strings.TrimSpace(l); l != "" { + return l[:min(len(l), 120)] + } + } + return "" + } + for _, cause := range []string{"the real cause", ""} { + out := sdkstep.Clip(lines, cause) + if len(out) > sdkstep.OutputBytes+sdkstep.LineBytes { + t.Errorf("1901/AC1: kept %d bytes, bound is %d", len(out), sdkstep.OutputBytes) + } + if !strings.HasSuffix(out, "the real cause\n--- FAIL: TestX (0.00s)") || !strings.Contains(out, "\n... (") { + t.Errorf("1901/AC1: the last lines or the marker are lost: %q...%q", out[:40], out[len(out)-60:]) + } + if got := oldFirst(out); strings.HasPrefix(got, "... (") || got == "" || (cause != "" && got != cause) { + t.Errorf("1901/AC3: cause %q: an older checker would read %q", cause, got) + } + } + }) + + // The Go reader (moved from mesh-build-go): an older checker, which knows no "cause" field, still reads a failure and + // gets at least the lines it kept (1901/AC3); a test's indented log line saying "panic:" is not the cause, go's own at + // column 0 is (1901/AC4); and each package counts its own skipped tests (issue 890). + t.Run("the Go reader", func(t *testing.T) { + read := func(name string) string { + raw, err := captured.ReadFile("captured/go/" + name + ".jsonl") + if err != nil { + t.Fatal(err) + } + return string(raw) + } + b, _ := json.Marshal(buildstep.ReadGoTest(read("logthenfatal"), ".").Failed) + var old []struct { + Output string `json:"output,omitempty"` + } + if err := json.Unmarshal(b, &old); err != nil || len(old) != 1 || !strings.Contains(old[0].Output, "judged against") || !strings.Contains(old[0].Output, "3 subscription(s)") { + t.Errorf("1901/AC3: an older checker reads %+v, %v", old, err) + } + lines := strings.SplitAfter(read("deeptimeout"), "\n") + added := `{"Action":"output","Package":"example.com/deeptimeout","Test":"TestHoldsTheSeat","Output":" p_test.go:9: the child said:\n"}` + "\n" + + `{"Action":"output","Package":"example.com/deeptimeout","Test":"TestHoldsTheSeat","Output":" panic: child retried and recovered\n"}` + "\n" + + `{"Action":"output","Package":"example.com/deeptimeout","Test":"TestHoldsTheSeat","Output":" WARNING: DATA RACE\n"}` + "\n" + if got := buildstep.ReadGoTest(strings.Join(lines[:10], "")+added+strings.Join(lines[10:], ""), ".").Failed; len(got) != 1 || !strings.HasPrefix(got[0].Cause, "panic: test timed out after 2s") { + t.Errorf("1901/AC4: failures = %+v, want the timeout's panic as the cause", got) + } + res := buildstep.ReadGoTest(`{"Action":"run","Package":"m/a","Test":"TestA"} +{"Action":"skip","Package":"m/a","Test":"TestA","Elapsed":0} +{"Action":"run","Package":"m/b","Test":"TestB"} +{"Action":"pass","Package":"m/b","Test":"TestB","Elapsed":0} +{"Action":"pass","Package":"m/a","Elapsed":0} +{"Action":"pass","Package":"m/b","Elapsed":0} +`, ".") + got := map[string]int{} + for _, p := range res.Packages { + got[p.Package] = p.Skipped + } + if res.Skipped != 1 || got["m/a"] != 1 || got["m/b"] != 0 { + t.Errorf("skipped = %d, per package %v, want 1 with m/a 1 and m/b 0", res.Skipped, got) + } + }) +} diff --git a/modules/mesh-build-go/buildstep/conformance/leftrunning.go b/modules/mesh-build-go/buildstep/conformance/leftrunning.go new file mode 100644 index 00000000..a8812c4d --- /dev/null +++ b/modules/mesh-build-go/buildstep/conformance/leftrunning.go @@ -0,0 +1,212 @@ +package conformance + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "strings" + "sync" + "testing" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/buildstep" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// **A step left running by a previous process** (novox/hq issue 1890, note 21): when a delivery replaced mesh-build-go +// on a node while a check ask ran, the new process's start sweep let the ask's key stand as running, and it held one +// of the node's build slots until a hand end. Every build module runs this case (LeftRunning) through the start sweep +// its main runs: the hand-started steps a previous process left are ended at once and loud, its asks' keys deleted (the +// stream redelivers the ask, novox/hq ADR 0525 §6, and its asker keeps waiting for that), what this process runs kept. A +// catalogue check (mesh-catalog checks/failurekit) refuses a build module whose tests do not call LeftRunning. + +// Sweep is a build module's own start sweep, as its main runs it once at its start: it ends node's steps in scan that +// no process of the module runs any more (buildstep.EndStaleAtStart, here with the runtime already serving), before +// being the start of the process, saying each end with logf. +type Sweep func(ctx context.Context, scan buildstep.Scan, node string, before time.Time, logf func(string, ...any)) + +// LeftNode is the node the case's steps are of. +const LeftNode = "kitnode" + +// leftCase is one step in the steps state before the sweep, and whether the sweep must end it. +type leftCase struct { + id, tool, owner, state string + node string + age time.Duration // started this long before the process began (negative: after it) + ended bool + deleted bool // a gone process's ask: its key is deleted, not ended +} + +// gone is a previous process's owner on the same node. +var gone = "b00t-of-the-kit.4242.mvp0000" + +func leftCases() []leftCase { + return []leftCase{ + {id: "mvpwlkzi7e6ca8", tool: "check ask", owner: gone, age: 5 * time.Minute, deleted: true}, // note 21's ghost + {id: "mvpwlkzi7e6ca9", tool: "delivery ask", owner: gone, age: -time.Second, deleted: true}, + {id: "gonehand1", tool: "build_x_test", owner: gone, age: time.Minute, ended: true}, + {id: "oldhand1", tool: "build_x_fetch", age: time.Hour, ended: true}, // no owner: an older build's + {id: "mvpwlkzi7e6caa", tool: "check ask", age: time.Hour, ended: true}, // no owner, past the bound + {id: "mvpwlkzi7e6cab", tool: "check ask", age: 5 * time.Minute}, // no owner, the stream's still + {id: "mvpwlkzi7e6cac", tool: "check ask", owner: buildstep.Owner, age: time.Minute}, // this process's + {id: "minehand1", tool: "build_x_test", owner: buildstep.Owner, age: -time.Second}, + {id: "donehand1", tool: "build_x_test", owner: gone, age: time.Hour, state: delivery.StepPassed}, + {id: "otherhand1", tool: "build_x_test", owner: gone, age: time.Hour, node: "kitother"}, + } +} + +// LeftRunning runs the case through sweep and fails the test for each step the sweep ended or kept wrongly, or ended +// without saying it. +func LeftRunning(t *testing.T, sweep Sweep) { + t.Helper() + for _, problem := range JudgeLeftRunning(sweep) { + t.Error(problem) + } +} + +// JudgeLeftRunning says what is wrong with what sweep did to the case; nothing when it keeps the rule. +func JudgeLeftRunning(sweep Sweep) []string { + st := &steps{vals: map[string][]byte{}, revs: map[string]uint64{}} + before := time.Now() + for _, c := range leftCases() { + node, state := c.node, c.state + if node == "" { + node = LeftNode + } + if state == "" { + state = delivery.StepRunning + } + key, _ := delivery.StepKey(node, c.id) + raw, err := delivery.EncodeStep(delivery.Step{ID: c.id, Tool: c.tool, Node: node, State: state, Started: before.Add(-c.age).UTC(), Owner: c.owner}) + if err != nil { + return []string{"the kit's step " + c.id + ": " + err.Error()} + } + st.set(key, raw) + } + served := make(chan struct{}) + close(served) + was := buildstep.StaleServed + buildstep.StaleServed = func() <-chan struct{} { return served } + defer func() { buildstep.StaleServed = was }() + var mu sync.Mutex + var said []string + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + sweep(ctx, st.scan(), LeftNode, before, func(f string, a ...any) { + mu.Lock() + defer mu.Unlock() + said = append(said, fmt.Sprintf(f, a...)) + }) + mu.Lock() + log := strings.Join(said, "\n") + mu.Unlock() + var problems []string + for _, c := range leftCases() { + node := c.node + if node == "" { + node = LeftNode + } + key, _ := delivery.StepKey(node, c.id) + raw := st.get(key) + if c.deleted { + if raw != nil { + problems = append(problems, fmt.Sprintf("left running: the ask %s of the gone owner %q is still held after the sweep (%s); "+ + "its key is deleted, never ended: the stream redelivers it (ADR 0525 §6)", key, c.owner, raw)) + } + if !strings.Contains(log, key) || !strings.Contains(log, c.owner) { + problems = append(problems, "left running: the delete of "+key+" is not said with its owner "+c.owner) + } + continue + } + if raw == nil { + problems = append(problems, "left running: the step "+key+" is gone after the sweep, want it kept or ended") + continue + } + s, err := delivery.DecodeStep(raw) + if err != nil { + problems = append(problems, key+": "+err.Error()) + continue + } + want := c.state + if want == "" { + want = delivery.StepRunning + } + if c.ended { + want = delivery.StepFailed + } + if s.State != want { + problems = append(problems, fmt.Sprintf("left running: the step %s (%s, owner %q, %s before the start) is %s after the sweep, want %s", key, c.tool, c.owner, c.age, s.State, want)) + } + if c.ended && !strings.Contains(log, key) { + problems = append(problems, "left running: the end of "+key+" is not said") + } + if c.ended && c.owner != "" && !strings.Contains(log, c.owner) { + problems = append(problems, "left running: the end of "+key+" does not name its owner "+c.owner) + } + } + sort.Strings(problems) + return problems +} + +// steps is a steps state with revisions, whose Update lands only at the revision read (stdio.ErrChanged otherwise). +type steps struct { + mu sync.Mutex + vals map[string][]byte + revs map[string]uint64 +} + +func (s *steps) set(key string, raw []byte) { + s.mu.Lock() + defer s.mu.Unlock() + s.vals[key], s.revs[key] = raw, s.revs[key]+1 +} + +func (s *steps) get(key string) []byte { + s.mu.Lock() + defer s.mu.Unlock() + return s.vals[key] +} + +func (s *steps) scan() buildstep.Scan { + return buildstep.Scan{ + Keys: func() ([]string, error) { + s.mu.Lock() + defer s.mu.Unlock() + var out []string + for k := range s.vals { + out = append(out, k) + } + return out, nil + }, + Get: func(key string) (json.RawMessage, uint64, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.vals[key], s.revs[key], nil + }, + Delete: func(key string, revision uint64) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.revs[key] != revision { + return stdio.ErrChanged + } + delete(s.vals, key) + s.revs[key]++ + return nil + }, + Update: func(key string, value any, revision uint64) (uint64, error) { + raw, err := json.Marshal(value) + if err != nil { + return 0, err + } + s.mu.Lock() + defer s.mu.Unlock() + if s.revs[key] != revision { + return 0, stdio.ErrChanged + } + s.vals[key], s.revs[key] = raw, revision+1 + return revision + 1, nil + }, + } +} diff --git a/modules/mesh-build-go/buildstep/cpu.go b/modules/mesh-build-go/buildstep/cpu.go new file mode 100644 index 00000000..d0e41b80 --- /dev/null +++ b/modules/mesh-build-go/buildstep/cpu.go @@ -0,0 +1,14 @@ +package buildstep + +import ( + "os" + + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/buildcpu" +) + +// NodeLimits is what one step of a build module is held to (novox/hq ADR 0553 §4): the node's processor share from the +// word the composer gives the module (buildcpu.ShareWord), divided by the module's own capacity, own, over the machine's +// processors. Without the word, no thread limit: Threads is 0. +func NodeLimits(own int) buildcpu.Limits { + return buildcpu.FromWords(os.Getenv, own) +} diff --git a/modules/mesh-build-go/buildstep/digestshape_test.go b/modules/mesh-build-go/buildstep/digestshape_test.go new file mode 100644 index 00000000..aa2b757d --- /dev/null +++ b/modules/mesh-build-go/buildstep/digestshape_test.go @@ -0,0 +1,234 @@ +package buildstep + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sort" + "strings" + "sync" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// credentialField is the node runtime's rule for a state value (mesh-tools node-tools/internal/bus/state.go, novox/hq +// ADR 0201), copied whole: the first field anywhere in the value whose name, lower-cased and without - _ and spaces, ends +// in a credential word, or "". A step's end is refused when it answers a field. +func credentialField(value json.RawMessage) string { + endings := []string{"password", "passwd", "secret", "token", "credential", "credentials", + "authorization", "apikey", "privatekey", "accesskey", "cookie"} + var v any + if json.Unmarshal(value, &v) != nil { + return "" + } + var walk func(any) string + walk = func(v any) string { + switch t := v.(type) { + case map[string]any: + keys := make([]string, 0, len(t)) + for k := range t { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + norm := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(k)) + for _, end := range endings { + if strings.HasSuffix(norm, end) { + return k + } + } + if found := walk(t[k]); found != "" { + return found + } + } + case []any: + for _, x := range t { + if found := walk(x); found != "" { + return found + } + } + } + return "" + } + return walk(v) +} + +// runtimeState is a steps state that refuses a value as the node runtime does, and keeps every value it took. +type runtimeState struct { + mu sync.Mutex + took map[string][]delivery.Step + refused int +} + +func newRuntimeState() *runtimeState { return &runtimeState{took: map[string][]delivery.Step{}} } + +func (s *runtimeState) put(key string, value any) (uint64, error) { + raw, _ := value.(json.RawMessage) + if f := credentialField(raw); f != "" { + s.mu.Lock() + s.refused++ + s.mu.Unlock() + return 0, fmt.Errorf("steps.%s carries a field %q, which names a credential", key, f) + } + st, err := delivery.DecodeStep(raw) + if err != nil { + return 0, err + } + s.mu.Lock() + defer s.mu.Unlock() + s.took[key] = append(s.took[key], st) + return uint64(len(s.took[key])), nil +} + +func (s *runtimeState) last(key string) (delivery.Step, bool) { + s.mu.Lock() + defer s.mu.Unlock() + t := s.took[key] + if len(t) == 0 { + return delivery.Step{}, false + } + return t[len(t)-1], true +} + +func fastEnds(t *testing.T) { + attempts, retry := EndAttempts, EndRetry + EndAttempts, EndRetry = 2, time.Millisecond + t.Cleanup(func() { EndAttempts, EndRetry = attempts, retry }) +} + +// 1930/AC1: given a check of a repository with the package .../internal/token, when its ask's answer is stored as the +// step's end, then no field is keyed by a package path: the runtime's credential rule takes the value, and the package +// and its digest read back from the stored end. +func TestADigestOfAPackageNamedTokenIsStoredAsAList(t *testing.T) { + fastEnds(t) + r := &recorder{ + fetch: map[string]any{"workspace": "/w"}, + digests: map[string]string{"x/internal/token": "d-token", "x/pkg/a": "d-a"}, + tests: map[string]bool{"x/internal/token": true, "x/pkg/a": true}, + } + out, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, + Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})) + if err != nil { + t.Fatal(err) + } + old, _ := json.Marshal(map[string]any{"levels": []any{map[string]any{"level": "unit", "digest": map[string]any{"packages": r.digests}}}}) + if credentialField(old) != "x/internal/token" { + t.Fatalf("the rule does not refuse the form before issue 1930 (%s): the test proves nothing", old) + } + st := newRuntimeState() + if err := finish(st.put, "shanks.a1", delivery.Step{ID: "a1", Tool: "check ask", Node: "shanks", State: delivery.StepRunning, Started: time.Now()}, out, nil, t.Logf); err != nil { + t.Fatal(err) + } + s, ok := st.last("shanks.a1") + if !ok || s.State != delivery.StepPassed || st.refused != 0 { + t.Fatalf("the end %+v (written %v, refused %d), want it passed and taken at once", s, ok, st.refused) + } + var a delivery.CheckAnswer + if err := json.Unmarshal(s.Result, &a); err != nil { + t.Fatal(err) + } + if len(a.Levels) != 1 || a.Levels[0].Digest == nil { + t.Fatalf("levels %+v, want the unit level with its digest", a.Levels) + } + want := []delivery.PackageDigest{{Package: "x/internal/token", Digest: "d-token"}, {Package: "x/pkg/a", Digest: "d-a"}} + if got := a.Levels[0].Digest.Packages; fmt.Sprint(got) != fmt.Sprint(want) { + t.Errorf("packages %v, want %v", got, want) + } + if !strings.Contains(string(s.Result), `"package":"x/internal/token"`) { + t.Errorf("the stored end does not name the package as a value: %s", s.Result) + } +} + +// 1930/AC2: given an end the state refuses every time, when finish writes it, then it logs the refusal and writes a small +// failed end without the result that names the write's error; and when no end can be written at all it answers an error. +func TestARefusedEndIsSaidAndEndsTheStepFailed(t *testing.T) { + fastEnds(t) + big := errors.New("the value carries a field \"x/internal/token\", which names a credential") + var refused int + var stored []delivery.Step + put := func(key string, value any) (uint64, error) { + st, _ := delivery.DecodeStep(value.(json.RawMessage)) + if len(st.Result) > 0 { + refused++ + return 0, big + } + stored = append(stored, st) + return 1, nil + } + var logged []string + logf := func(format string, args ...any) { logged = append(logged, fmt.Sprintf(format, args...)) } + err := finish(put, "shanks.a2", delivery.Step{ID: "a2", Tool: "check ask", Node: "shanks", State: delivery.StepRunning, Started: time.Now()}, map[string]any{"levels": 1}, nil, logf) + if err != nil { + t.Fatalf("finish answered %v, want the small end written", err) + } + if refused != EndAttempts || len(stored) != 1 { + t.Fatalf("refused %d, stored %d, want %d refusals and one small end", refused, len(stored), EndAttempts) + } + s := stored[0] + if s.State != delivery.StepFailed || len(s.Result) != 0 || !strings.Contains(s.Reason, "could not be written") || + !strings.Contains(s.Reason, big.Error()) || !strings.Contains(s.Reason, "passed") { + t.Errorf("the small end %+v, want failed, no result, naming the write's error and that it passed", s) + } + if len(logged) == 0 || !strings.Contains(logged[0], big.Error()) { + t.Errorf("logged %q, want the refusal said", logged) + } + none := func(string, any) (uint64, error) { return 0, errors.New("the bus is gone") } + if err := finish(none, "shanks.a3", delivery.Step{ID: "a3", Tool: "check ask", Node: "shanks", Started: time.Now()}, nil, nil, logf); err == nil || !strings.Contains(err.Error(), "the bus is gone") { + t.Errorf("finish with no write possible answered %v, want the error", err) + } +} + +// 1930/AC2: given an ask whose end the state refuses, when it runs, then the ask is acknowledged only once its small +// failed end is written; when no end can be written at all, it is given back, never acknowledged as done. +func TestAnAskWhoseEndCannotBeWrittenIsNotAcked(t *testing.T) { + fastEnds(t) + for _, tc := range []struct { + name string + smallOK bool + wantAcked bool + }{{"the small end written", true, true}, {"no end written", false, false}} { + t.Run(tc.name, func(t *testing.T) { + src := NewFakeAsks() + a := testAsk(delivery.AskCheck) + _ = src.PublishAsk(a) + var mu sync.Mutex + var ends []delivery.Step + put := func(key string, value any) (uint64, error) { + st, err := delivery.DecodeStep(value.(json.RawMessage)) + if err != nil { + return 0, err + } + if st.State != delivery.StepRunning && (len(st.Result) > 0 || !tc.smallOK) { + return 0, errors.New("refused") + } + mu.Lock() + ends = append(ends, st) + mu.Unlock() + return 1, nil + } + p := &Puller{Source: src, Language: delivery.AskGo, Node: "shanks", Put: put, Capacity: func() int { return 1 }, + PullWait: 10 * time.Millisecond, Logf: t.Logf, + Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return map[string]any{"ran": true}, nil }} + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go p.Pull(ctx) + waitFor(t, "the ask's fate", func() bool { ac, tm, nk := src.Fates(); return len(ac)+len(tm)+len(nk) > 0 }) + cancel() + acked, termed, naked := src.Fates() + if tc.wantAcked && (len(acked) != 1 || len(naked) != 0) || !tc.wantAcked && (len(acked) != 0 || len(naked) == 0) || len(termed) != 0 { + t.Fatalf("acked %v naked %v termed %v", acked, naked, termed) + } + mu.Lock() + defer mu.Unlock() + if tc.smallOK { + last := ends[len(ends)-1] + if last.State != delivery.StepFailed || !strings.Contains(last.Reason, "refused") { + t.Errorf("the end %+v, want the small failed end", last) + } + } + }) + } +} diff --git a/modules/mesh-build-go/buildstep/fetch.go b/modules/mesh-build-go/buildstep/fetch.go new file mode 100644 index 00000000..4135bdba --- /dev/null +++ b/modules/mesh-build-go/buildstep/fetch.go @@ -0,0 +1,196 @@ +package buildstep + +import ( + "context" + "fmt" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" + + "git.novox.be/novox/mesh-sdk/go/forgekey" +) + +// Option changes FetchCommit. +type Option func(*fetchOptions) + +type fetchOptions struct { + allowFile bool + mirror string +} + +// AllowFileRemote lets FetchCommit take a file:// remote. It is for a test's local repository; production +// code does not pass it, and FetchCommit then refuses anything but ssh://. +func AllowFileRemote() Option { return func(o *fetchOptions) { o.allowFile = true } } + +// WithMirror makes FetchCommit (and MergeInto) borrow objects from mirror, a bare repository that UpdateMirror keeps +// in the build module's own cache (novox/hq ADR 0536 §2; issue 890): the workspace's .git/objects/info/alternates +// names the mirror's objects directory, so git transfers only the objects the mirror lacks. It is a plain file, never +// a symlink. mirror must be an absolute path of an existing directory; the workspace never writes to it. +func WithMirror(mirror string) Option { return func(o *fetchOptions) { o.mirror = mirror } } + +// useMirror writes the alternates file of the repository in dir, when a mirror is set. +func useMirror(o fetchOptions, dir string) error { + if o.mirror == "" { + return nil + } + info := filepath.Join(dir, ".git", "objects", "info") + if err := os.MkdirAll(info, 0o755); err != nil { + return fmt.Errorf("buildstep: the mirror cannot be used: %w", err) + } + line := filepath.Join(o.mirror, "objects") + "\n" + if err := os.WriteFile(filepath.Join(info, "alternates"), []byte(line), 0o644); err != nil { + return fmt.Errorf("buildstep: the mirror cannot be used: %w", err) + } + return nil +} + +var ( + commitOK = regexp.MustCompile(`^[0-9a-f]{40}$`) + refOK = regexp.MustCompile(`^refs/[A-Za-z0-9._/-]+$`) +) + +// FetchCommit fetches one commit of a forge repository into dir, an empty directory, and checks it out detached +// (novox/hq ADR 0466 §4, §5; issue 1474). remote is ssh:////.git, the ssh name being the +// one the build module's templates set up (ADR 0409 §1); commit is a full 40-hex sha; ref is optional, and names +// the pull request's head (refs/pull//head) when the commit is only there. Every git command runs through run +// as `git -C dir ...` with GIT_TERMINAL_PROMPT=0 and no credential helper, so nothing is asked for and no +// credential is used. A remote that is not ssh://, or one with user information, and a commit that is not 40 +// hex digits are refused before anything runs. +func FetchCommit(ctx context.Context, run forgekey.Runner, dir, remote, commit, ref string, opts ...Option) error { + var o fetchOptions + for _, opt := range opts { + opt(&o) + } + if err := checkFetch(o, dir, remote, commit, ref); err != nil { + return err + } + env := []string{"GIT_TERMINAL_PROMPT=0"} + git := func(args ...string) (string, error) { + full := append([]string{"-c", "credential.helper=", "-C", dir}, args...) + r := run(ctx, env, "git", full...) + if r.Status != 0 || r.Err != "" { + return "", fmt.Errorf("git %s: %s", args[0], forgekey.Said(r.Err+"\n"+r.Stderr)) + } + return r.Stdout, nil + } + if _, err := git("init", "-q"); err != nil { + return err + } + if err := useMirror(o, dir); err != nil { + return err + } + if _, err := git("remote", "add", "origin", remote); err != nil { + return err + } + if _, err := git("fetch", "--no-tags", "--depth", "1", "origin", commit); err != nil { + if ref == "" { + return err + } + if _, rerr := git("fetch", "--no-tags", "origin", ref); rerr != nil { + return fmt.Errorf("%v; then the ref %s: %w", err, ref, rerr) + } + if _, verr := git("cat-file", "-e", commit+"^{commit}"); verr != nil { + return fmt.Errorf("the commit %s is not in %s: %w", commit, ref, verr) + } + } + if _, err := git("checkout", "-q", "--detach", commit); err != nil { + return err + } + out, err := git("rev-parse", "HEAD") + if err != nil { + return err + } + if head := strings.TrimSpace(out); head != commit { + return fmt.Errorf("the checkout is at %s, not at the commit %s", head, commit) + } + return nil +} + +// UpdateMirror brings the bare repository mirror to commit: it makes the mirror when it is absent (git init --bare, +// origin set to remote), then fetches the commit, or the ref when only the ref holds it, with the same rules as +// FetchCommit (no prompt, no credential helper, ssh:// or an allowed file:// remote, a 40-hex commit). The mirror +// keeps every commit it was brought to, so later fetches through WithMirror transfer only what is new. Callers +// serialise updates of one mirror. +func UpdateMirror(ctx context.Context, run forgekey.Runner, mirror, remote, commit, ref string, opts ...Option) error { + var o fetchOptions + for _, opt := range opts { + opt(&o) + } + if !filepath.IsAbs(mirror) { + return fmt.Errorf("buildstep: the mirror %q is not an absolute path", mirror) + } + if err := checkFetch(o, mirror, remote, commit, ref); err != nil { + return err + } + env := []string{"GIT_TERMINAL_PROMPT=0"} + git := func(args ...string) error { + r := run(ctx, env, "git", append([]string{"-c", "credential.helper="}, args...)...) + if r.Status != 0 || r.Err != "" { + verb := args[0] + if verb == "-C" && len(args) > 2 { // the mirror's commands are named by what they do, not by -C + verb = args[2] + } + return fmt.Errorf("git %s: %s", verb, forgekey.Said(r.Err+"\n"+r.Stderr)) + } + return nil + } + in := func(args ...string) error { return git(append([]string{"-C", mirror}, args...)...) } + if _, err := os.Stat(filepath.Join(mirror, "HEAD")); err != nil { + if err := git("init", "-q", "--bare", mirror); err != nil { + return err + } + if err := in("remote", "add", "origin", remote); err != nil { + return err + } + } else if err := in("remote", "set-url", "origin", remote); err != nil { + return err + } + if err := in("fetch", "--no-tags", "origin", commit); err != nil { + if ref == "" { + return err + } + if rerr := in("fetch", "--no-tags", "origin", ref); rerr != nil { + return fmt.Errorf("%v; then the ref %s: %w", err, ref, rerr) + } + } + if err := in("cat-file", "-e", commit+"^{commit}"); err != nil { + return fmt.Errorf("the commit %s is not in the mirror: %w", commit, err) + } + return nil +} + +func checkFetch(o fetchOptions, dir, remote, commit, ref string) error { + if dir == "" { + return fmt.Errorf("buildstep: no workspace directory") + } + if o.mirror != "" { + if !filepath.IsAbs(o.mirror) { + return fmt.Errorf("buildstep: the mirror %q is not an absolute path", o.mirror) + } + if fi, err := os.Stat(o.mirror); err != nil || !fi.IsDir() { + return fmt.Errorf("buildstep: the mirror %q is not an existing directory", o.mirror) + } + } + if !commitOK.MatchString(commit) { + return fmt.Errorf("buildstep: %q is not a full commit (40 hex digits)", commit) + } + if ref != "" && !refOK.MatchString(ref) { + return fmt.Errorf("buildstep: %q is not a ref (refs/...)", ref) + } + u, err := url.Parse(remote) + if err != nil { + return fmt.Errorf("buildstep: the remote is not a URL: %w", err) + } + if u.User != nil { + return fmt.Errorf("buildstep: the remote carries user information; the key is the credential") + } + switch { + case u.Scheme == "ssh" && u.Host != "": + case u.Scheme == "file" && o.allowFile: + default: + return fmt.Errorf("buildstep: the remote %q is not ssh://", remote) + } + return nil +} diff --git a/modules/mesh-build-go/buildstep/fetch_integration_test.go b/modules/mesh-build-go/buildstep/fetch_integration_test.go new file mode 100644 index 00000000..5f782a1f --- /dev/null +++ b/modules/mesh-build-go/buildstep/fetch_integration_test.go @@ -0,0 +1,59 @@ +//go:build integration + +package buildstep + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/forgekey" +) + +func gitIn(t *testing.T, dir string, args ...string) string { + t.Helper() + cmd := exec.Command("git", append([]string{"-C", dir, "-c", "user.name=t", "-c", "user.email=t@example.invalid"}, args...)...) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git %v: %v\n%s", args, err, out) + } + return strings.TrimSpace(string(out)) +} + +// 1474/AC5: a local bare repository with a commit only under refs/pull/1/head (a file:// remote, allowed by the +// test-only option): the workspace's HEAD is the commit. The forge itself is proven live after the build +// modules' delivery (ADR 0466 §5). +func TestFetchCommitFromAPullRefOnly(t *testing.T) { + root := t.TempDir() + bare := filepath.Join(root, "r.git") + work := filepath.Join(root, "work") + for _, d := range []string{bare, work} { + if err := os.MkdirAll(d, 0o755); err != nil { + t.Fatal(err) + } + } + gitIn(t, bare, "init", "-q", "--bare") + gitIn(t, work, "init", "-q") + if err := os.WriteFile(filepath.Join(work, "f"), []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + gitIn(t, work, "add", "f") + gitIn(t, work, "commit", "-q", "-m", "pull request head") + commit := gitIn(t, work, "rev-parse", "HEAD") + gitIn(t, work, "push", "-q", bare, "HEAD:refs/pull/1/head") + + ws := filepath.Join(root, "ws") + if err := os.MkdirAll(ws, 0o755); err != nil { + t.Fatal(err) + } + err := FetchCommit(context.Background(), forgekey.ExecRunner, ws, "file://"+bare, commit, "refs/pull/1/head", AllowFileRemote()) + if err != nil { + t.Fatal(err) + } + if head := gitIn(t, ws, "rev-parse", "HEAD"); head != commit { + t.Errorf("HEAD = %s, want %s", head, commit) + } +} diff --git a/modules/mesh-build-go/buildstep/fetch_test.go b/modules/mesh-build-go/buildstep/fetch_test.go new file mode 100644 index 00000000..b989ab6d --- /dev/null +++ b/modules/mesh-build-go/buildstep/fetch_test.go @@ -0,0 +1,137 @@ +package buildstep + +import ( + "context" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/forgekey" +) + +const sha = "0123456789abcdef0123456789abcdef01234567" + +// fakeGit records every command and answers by a rule. +type fakeGit struct { + calls []string + envs [][]string + rule func(args string) forgekey.Ran +} + +func (f *fakeGit) run(ctx context.Context, env []string, name string, args ...string) forgekey.Ran { + line := name + " " + strings.Join(args, " ") + f.calls = append(f.calls, line) + f.envs = append(f.envs, env) + if f.rule != nil { + if r := f.rule(line); r.Status != 0 || r.Stdout != "" || r.Err != "" { + return r + } + } + if len(args) > 4 && args[4] == "rev-parse" { + return forgekey.Ran{Stdout: sha + "\n"} + } + return forgekey.Ran{} +} + +const pre = "git -c credential.helper= -C /w " + +// 1474/AC1: the git commands are exactly the sequence, none with a credential or an https remote. +func TestFetchCommitRunsTheSequence(t *testing.T) { + f := &fakeGit{} + if err := FetchCommit(context.Background(), f.run, "/w", "ssh://mesh-forge/novox/mesh-sdk.git", sha, ""); err != nil { + t.Fatal(err) + } + want := []string{ + pre + "init -q", + pre + "remote add origin ssh://mesh-forge/novox/mesh-sdk.git", + pre + "fetch --no-tags --depth 1 origin " + sha, + pre + "checkout -q --detach " + sha, + pre + "rev-parse HEAD", + } + if strings.Join(f.calls, "\n") != strings.Join(want, "\n") { + t.Errorf("commands:\n%s\nwant:\n%s", strings.Join(f.calls, "\n"), strings.Join(want, "\n")) + } + for i, c := range f.calls { + if strings.Contains(c, "https://") || strings.Contains(c, "@") || !strings.Contains(c, "credential.helper= ") { + t.Errorf("command %q carries a credential or https", c) + } + if len(f.envs[i]) != 1 || f.envs[i][0] != "GIT_TERMINAL_PROMPT=0" { + t.Errorf("env %v", f.envs[i]) + } + } +} + +// 1474/AC2: the depth-1 fetch fails and a pull ref is given: the ref is fetched and the commit verified; with no +// ref, the fetch's failure is the answer. +func TestFetchCommitFallsBackToThePullRef(t *testing.T) { + f := &fakeGit{rule: func(l string) forgekey.Ran { + if strings.Contains(l, "--depth 1") { + return forgekey.Ran{Status: 128, Stderr: "fatal: couldn't find remote ref " + sha + "\n"} + } + return forgekey.Ran{} + }} + if err := FetchCommit(context.Background(), f.run, "/w", "ssh://mesh-forge/o/r.git", sha, "refs/pull/1/head"); err != nil { + t.Fatal(err) + } + want := []string{pre + "fetch --no-tags origin refs/pull/1/head", pre + "cat-file -e " + sha + "^{commit}", pre + "checkout -q --detach " + sha} + got := strings.Join(f.calls[3:6], "\n") + if got != strings.Join(want, "\n") { + t.Errorf("after the failed fetch:\n%s\nwant:\n%s", got, strings.Join(want, "\n")) + } + + g := &fakeGit{rule: f.rule} + err := FetchCommit(context.Background(), g.run, "/w", "ssh://mesh-forge/o/r.git", sha, "") + if err == nil || !strings.Contains(err.Error(), "couldn't find remote ref") || len(g.calls) != 3 { + t.Errorf("without a ref: %v after %d commands", err, len(g.calls)) + } + + h := &fakeGit{rule: func(l string) forgekey.Ran { + if strings.Contains(l, "--depth 1") || strings.Contains(l, "cat-file") { + return forgekey.Ran{Status: 1, Stderr: "no"} + } + return forgekey.Ran{} + }} + if err := FetchCommit(context.Background(), h.run, "/w", "ssh://mesh-forge/o/r.git", sha, "refs/pull/1/head"); err == nil { + t.Error("a commit that is not in the ref was accepted") + } +} + +// 1474/AC3: HEAD differs from the commit after the checkout: an error naming both. +func TestFetchCommitAnswersAHeadThatIsNotTheCommit(t *testing.T) { + other := strings.Repeat("a", 40) + f := &fakeGit{rule: func(l string) forgekey.Ran { + if strings.HasSuffix(l, "rev-parse HEAD") { + return forgekey.Ran{Stdout: other + "\n"} + } + return forgekey.Ran{} + }} + err := FetchCommit(context.Background(), f.run, "/w", "ssh://mesh-forge/o/r.git", sha, "") + if err == nil || !strings.Contains(err.Error(), other) || !strings.Contains(err.Error(), sha) { + t.Errorf("error = %v, want both shas", err) + } +} + +// 1474/AC4: an https remote, user information, a file remote without the option, or a commit that is not 40 +// hex digits is refused before anything runs. +func TestFetchCommitRefusesBeforeRunningAnything(t *testing.T) { + for _, c := range []struct{ remote, commit, ref string }{ + {"https://forge.example/o/r.git", sha, ""}, + {"http://forge.example/o/r.git", sha, ""}, + {"ssh://git@mesh-forge/o/r.git", sha, ""}, + {"ssh://user:pw@mesh-forge/o/r.git", sha, ""}, + {"file:///tmp/r.git", sha, ""}, + {"/tmp/r.git", sha, ""}, + {"ssh://mesh-forge/o/r.git", "abc123", ""}, + {"ssh://mesh-forge/o/r.git", strings.ToUpper(sha), ""}, + {"ssh://mesh-forge/o/r.git", "main", ""}, + {"ssh://mesh-forge/o/r.git", sha, "--upload-pack=x"}, + } { + f := &fakeGit{} + if err := FetchCommit(context.Background(), f.run, "/w", c.remote, c.commit, c.ref); err == nil || len(f.calls) != 0 { + t.Errorf("%+v: err %v, %d commands", c, err, len(f.calls)) + } + } + f := &fakeGit{} + if err := FetchCommit(context.Background(), f.run, "/w", "file:///tmp/r.git", sha, "", AllowFileRemote()); err != nil { + t.Errorf("a file remote with the option: %v", err) + } +} diff --git a/modules/mesh-build-go/buildstep/go.mod b/modules/mesh-build-go/buildstep/go.mod new file mode 100644 index 00000000..a629739e --- /dev/null +++ b/modules/mesh-build-go/buildstep/go.mod @@ -0,0 +1,22 @@ +module git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 + github.com/nats-io/nats.go v1.54.0 +) + +require ( + github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect + github.com/google/go-tpm v0.9.8 // indirect + github.com/klauspost/compress v1.20.0 // indirect + github.com/minio/highwayhash v1.0.4 // indirect + github.com/nats-io/jwt/v2 v2.8.1 // indirect + github.com/nats-io/nats-server/v2 v2.11.17 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/time v0.15.0 // indirect +) diff --git a/modules/mesh-build-go/buildstep/go.sum b/modules/mesh-build-go/buildstep/go.sum new file mode 100644 index 00000000..ebd4e0f2 --- /dev/null +++ b/modules/mesh-build-go/buildstep/go.sum @@ -0,0 +1,27 @@ +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= +github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= +github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/modules/mesh-build-go/buildstep/gotest.go b/modules/mesh-build-go/buildstep/gotest.go new file mode 100644 index 00000000..0f6b59bb --- /dev/null +++ b/modules/mesh-build-go/buildstep/gotest.go @@ -0,0 +1,410 @@ +package buildstep + +// The reader of `go test -json` (moved whole from mesh-catalog's mesh-build-go, issue 1901's thorough fix): per package +// what ran and for how long, and each failure under the contract (failure.go), its real cause too when the failure is no +// test's assertion (a panic, a timeout, a build error, an exit, a kill, a race). + +import ( + "bufio" + "encoding/json" + "fmt" + sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep" + "regexp" + "slices" + "strings" +) + +// GoPackage is one package's part of a Go test level. +type GoPackage struct { + Dir string `json:"dir"` + Package string `json:"package"` + Passed bool `json:"passed"` + Elapsed float64 `json:"elapsed"` + Tests int `json:"tests"` + // Skipped is how many of the package's tests skipped. + Skipped int `json:"skipped,omitempty"` + // NoTests is a package with no test files (at this level: a file with the other level's build tag is none). + NoTests bool `json:"no_tests,omitempty"` +} + +// TestTime is one test and the seconds it took. +type TestTime struct { + Package string `json:"package"` + Test string `json:"test"` + Elapsed float64 `json:"elapsed"` +} + +// GoTest is what one go.mod directory's `go test -json` said. +type GoTest struct { + Packages []GoPackage + Tests int + Skipped int + Times []TestTime + Failed []sdkstep.Failure + // Elapsed is the slowest package's running time in seconds. + Elapsed float64 + // Noise is the lines that were no event: what go said when it could not run the tests at all. + Noise []string +} + +// ReadGoTest reads the output of `go test -json` run in the go.mod directory dir. +func ReadGoTest(out, dir string) GoTest { + r := &goReader{dir: dir, pkgs: map[string]*GoPackage{}, builds: map[string][]string{}, pkgOut: map[string][]string{}, + testOut: map[string][]string{}, causes: map[string]string{}, failedIn: map[string]bool{}, + running: map[string][]string{}, passed: map[string]bool{}, built: map[string]bool{}} + sc := bufio.NewScanner(strings.NewReader(out)) + sc.Buffer(make([]byte, 1<<20), 1<<26) + for sc.Scan() { + var ev goEvent + if err := json.Unmarshal(sc.Bytes(), &ev); err != nil { + if l := strings.TrimSpace(sc.Text()); l != "" { + r.res.Noise = append(r.res.Noise, l) + } + continue + } + r.event(ev) + } + r.finish() + return r.res +} + +// goEvent is one line of `go test -json` (test2json, and the build events go test adds). +type goEvent struct { + Action string + Package string + Test string + Output string + OutputType string // "error" for a t.Error or t.Fatal message, "error-continue" for its further lines + ImportPath string + FailedBuild string + Elapsed float64 +} + +type goReader struct { + res GoTest + dir string + pkgs map[string]*GoPackage + order []string + builds map[string][]string // build output by import path + pkgOut map[string][]string + testOut map[string][]string // by package + "\x00" + test + causes map[string]string // the first error line of a test, by the same key + failedIn map[string]bool // packages with a failed test + // running is the tests that started and have not ended, by package, in the order they started: when a package + // fails with no failed test, these were running when its test program stopped. + running map[string][]string + // passed says a package's test program printed "PASS" before it failed; built says it failed to build. + passed, built map[string]bool +} + +func (r *goReader) pkg(name string) *GoPackage { + p := r.pkgs[name] + if p == nil { + p = &GoPackage{Dir: r.dir, Package: name} + r.pkgs[name] = p + r.order = append(r.order, name) + } + return p +} + +func keepLine(lines []string, out string) []string { + if t := strings.TrimRight(out, "\r\n"); t != "" && !strings.HasPrefix(t, "=== ") { + lines = append(lines, t) + } + return lines +} + +func (r *goReader) event(ev goEvent) { + switch { + case ev.Package == "" && ev.ImportPath != "": + if ev.Action == "build-output" { + r.builds[ev.ImportPath] = keepLine(r.builds[ev.ImportPath], ev.Output) + } + case ev.Package == "": + case ev.Test == "": + p := r.pkg(ev.Package) + switch ev.Action { + case "output": + // go test's own summary of a package is never a failure's words: a "PASS" before a "FAIL " says only + // that the tests passed and the program failed after them. + switch l := strings.TrimRight(ev.Output, "\r\n"); { + case l == "PASS": + r.passed[ev.Package] = true + case goSummaryLine(l): + default: + r.pkgOut[ev.Package] = keepLine(r.pkgOut[ev.Package], ev.Output) + } + case "pass": + p.Passed, p.Elapsed = true, ev.Elapsed + case "fail": + p.Passed, p.Elapsed = false, ev.Elapsed + if ev.FailedBuild != "" { + r.built[ev.Package] = true + r.pkgOut[ev.Package] = append(r.builds[ev.FailedBuild], r.pkgOut[ev.Package]...) + } + case "skip": + p.Passed, p.NoTests = true, true + } + default: + key := ev.Package + "\x00" + ev.Test + switch ev.Action { + case "output": + r.testOut[key] = keepLine(r.testOut[key], ev.Output) + if ev.OutputType == "error" && r.causes[key] == "" { + r.causes[key] = goErrorWords(ev.Output) + } + case "run": + r.running[ev.Package] = append(r.running[ev.Package], ev.Test) + case "pass", "fail", "skip": + r.running[ev.Package] = slices.DeleteFunc(r.running[ev.Package], func(t string) bool { return t == ev.Test }) + p := r.pkg(ev.Package) + p.Tests++ + r.res.Tests++ + if ev.Action == "skip" { + r.res.Skipped++ + p.Skipped++ + } + r.res.Times = append(r.res.Times, TestTime{Package: ev.Package, Test: ev.Test, Elapsed: ev.Elapsed}) + if ev.Action == "fail" { + r.failedIn[ev.Package] = true + if r.subtestFailed(ev.Package, ev.Test) { + return // a parent failed only by its subtest is that subtest's failure, said once + } + if r.subtestTakesTheCause(ev.Package, ev.Test) { + return // a subtest's panic, which go files under its parent, is said on the subtest + } + r.res.Failed = append(r.res.Failed, r.testFailure(ev.Package, ev.Test)) + } + } + } +} + +// subtestFailed says a failure of test under pkg was already reported for one of its subtests (they end first), each with +// a cause of its own, and the test reported no error and no runtime cause (a subtest's panic) of its own. +func (r *goReader) subtestFailed(pkg, test string) bool { + key := pkg + "\x00" + test + if r.causes[key] != "" || goRuntimeCause(r.testOut[key]) != "" { + return false + } + for _, f := range r.res.Failed { + if f.Package == pkg && strings.HasPrefix(f.Test, test+"/") { + return true + } + } + return false +} + +// noMessage is the cause of a test that failed and said nothing of why. +const noMessage = "the test failed without an error message (t.Fail or t.FailNow)" + +// subtestTakesTheCause gives a parent's runtime cause (a subtest's panic: go test -json files its lines under the +// parent) to the subtests of that parent that failed with no message of their own, and says it did. A parent with an +// error line of its own, or whose failed subtests each said why, is reported as itself. +func (r *goReader) subtestTakesTheCause(pkg, test string) bool { + key := pkg + "\x00" + test + if r.causes[key] != "" { + return false + } + cause := goRuntimeCause(r.testOut[key]) + if cause == "" { + return false + } + took := false + for i, f := range r.res.Failed { + if f.Package == pkg && strings.HasPrefix(f.Test, test+"/") && f.Cause == noMessage { + r.res.Failed[i].Cause, r.res.Failed[i].Output = cause, sdkstep.Clip(r.testOut[key], cause) + took = true + } + } + return took +} + +// testFailure is a test that failed: its cause is its first error line, the race it ran into, the runtime's line, or +// words saying it failed without a message. +func (r *goReader) testFailure(pkg, test string) sdkstep.Failure { + key := pkg + "\x00" + test + cause := r.causes[key] + if race := goRaceCause(r.testOut[key]); race != "" && (cause == "" || strings.HasPrefix(cause, "race detected")) { + cause = race // the race itself, not testing's "race detected during execution of test" + } else if cause == "" { + cause = goRuntimeCause(r.testOut[key]) + } + if cause == "" { + cause = noMessage + } + return sdkstep.Failure{Package: pkg, Test: test, Output: sdkstep.Clip(r.testOut[key], cause), Cause: cause} +} + +// goSummaryLine says a line is go test's summary of a package, in go's exact forms only: "PASS" or "FAIL" alone, +// "ok \t\t