From 2d2eb5dffb02d843028c3f15b943454db1bed561 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 18:20:39 +0100 Subject: [PATCH 01/16] mesh-provisioner: make every plan from the node-engine's published compositions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The node-engine alone composes (ADR 0593 §6, §8), so the provisioner stops composing with frombus and fromruntime and reads each module's plan from composed- (read as mesh-host.composed): at its want, or the build its have runs, and at the indexer's newest build, so a key ahead of its want still carries its plan entry. A refused or not yet published composition holds the module with its last plan kept. The golden test proves the plans and provisions are today's, captured from the composing holder before the change. It no longer reads the builds or the resolved values. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../mesh-provisioner/compose_bench_test.go | 225 ------------------ .../cmd/mesh-provisioner/composed_test.go | 204 ++++++++++++++++ .../cmd/mesh-provisioner/holder.go | 203 +++++++++++++--- .../holder_integration_test.go | 141 +++-------- .../cmd/mesh-provisioner/holder_test.go | 19 +- .../cmd/mesh-provisioner/holdertools.go | 14 +- .../cmd/mesh-provisioner/main.go | 9 +- .../cmd/mesh-provisioner/provisioner_test.go | 12 +- .../testdata/ahead-today-composed.json | 100 ++++++++ .../testdata/ahead-today-states.golden.json | 71 ++++++ .../testdata/today-composed.json | 110 +++++++++ .../testdata/today-states.golden.json | 73 ++++++ modules/mesh-provisioner/module.json | 3 +- 13 files changed, 790 insertions(+), 394 deletions(-) delete mode 100644 modules/mesh-provisioner/cmd/mesh-provisioner/compose_bench_test.go create mode 100644 modules/mesh-provisioner/cmd/mesh-provisioner/composed_test.go create mode 100644 modules/mesh-provisioner/cmd/mesh-provisioner/testdata/ahead-today-composed.json create mode 100644 modules/mesh-provisioner/cmd/mesh-provisioner/testdata/ahead-today-states.golden.json create mode 100644 modules/mesh-provisioner/cmd/mesh-provisioner/testdata/today-composed.json create mode 100644 modules/mesh-provisioner/cmd/mesh-provisioner/testdata/today-states.golden.json diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/compose_bench_test.go b/modules/mesh-provisioner/cmd/mesh-provisioner/compose_bench_test.go deleted file mode 100644 index 0b901b32..00000000 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/compose_bench_test.go +++ /dev/null @@ -1,225 +0,0 @@ -package main - -// BenchmarkComposeNode times ComposeNode of an unchanged node over a mesh captured from the bus (issue 1954): the -// placement, wants, haves, the indexer's modules and manifests, the builder's builds, mesh-config's resolved values and -// the templates archives, read with the read tools (mesh-placement.placement, mesh-delivery-view.assigner_wants, -// node_haves, indexer_modules, builder_build, mesh-config.config) and the manifests and archives by digest. The capture -// is not kept in this repository (a few megabytes, and the mesh's own addresses and keys): the benchmark reads the -// directory MESH_COMPOSE_CAPTURE names and is skipped without it. MESH_COMPOSE_NODE names the node (novox when unset). -// -// The capture's layout: placement.json, wants.json, haves.json, modules.json and config.json as those tools answer, -// builds/..json as builder_build answers, manifests/ and archives/ as their bytes. - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - "strings" - "sync/atomic" - "testing" - "time" - - stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/compose/frombus" - "git.novox.be/novox/mesh-sdk/go/compose/fromruntime" - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -// countingStore counts the holder's reads of its own states (and the providers' entries), each a bus request live. -type countingStore struct { - Store - gets, keys atomic.Int64 -} - -func (c *countingStore) Get(state, key string) (*Entry, error) { - c.gets.Add(1) - return c.Store.Get(state, key) -} - -func (c *countingStore) Keys(state string) ([]string, error) { - c.keys.Add(1) - return c.Store.Keys(state) -} - -// captured is the captured mesh as the states the holder reads, and the archives by digest. -func captured(b *testing.B, dir string) (map[string]map[string][]byte, map[string][]byte) { - b.Helper() - read := func(name string, into any) { - raw, err := os.ReadFile(filepath.Join(dir, name)) - if err != nil { - b.Fatal(err) - } - if err := json.Unmarshal(raw, into); err != nil { - b.Fatalf("%s: %v", name, err) - } - } - states := map[string]map[string][]byte{} - put := func(state, key string, raw []byte) { - if states[state] == nil { - states[state] = map[string][]byte{} - } - states[state][key] = raw - } - var placement struct { - Keys map[string]struct { - Placement json.RawMessage `json:"placement"` - } `json:"keys"` - } - read("placement.json", &placement) - for node, k := range placement.Keys { - var p delivery.Placement - if err := json.Unmarshal(k.Placement, &p); err != nil { - b.Fatal(err) - } - p.Node = node - raw, err := delivery.EncodePlacement(p) - if err != nil { - b.Fatal(err) - } - put(PlacementRead, "placement."+node, raw) - } - var wants struct { - Wants map[string]json.RawMessage `json:"wants"` - } - read("wants.json", &wants) - for k, v := range wants.Wants { - put(WantsRead, k, v) - } - var haves struct { - Keys map[string]json.RawMessage `json:"keys"` - } - read("haves.json", &haves) - for k, v := range haves.Keys { - put(HaveRead, k, v) - } - var modules struct { - Modules map[string]json.RawMessage `json:"modules"` - } - read("modules.json", &modules) - for m, v := range modules.Modules { - key, err := delivery.ModulesKey(m) - if err != nil { - b.Fatal(err) - } - put(ModulesRead, key, v) - } - var config struct { - Resolved []struct { - Key, Module, Node, Source string - Value any - } `json:"resolved"` - } - read("config.json", &config) - for _, r := range config.Resolved { - raw, _ := json.Marshal(map[string]any{"value": r.Value, "source": r.Source}) - put(ResolvedRead, r.Node+"."+r.Module+"."+r.Key, raw) - } - manifests, _ := filepath.Glob(filepath.Join(dir, "manifests", "*")) - for _, f := range manifests { - raw, err := os.ReadFile(f) - if err != nil { - b.Fatal(err) - } - digest := filepath.Base(f) - v, err := delivery.EncodeManifest(delivery.Manifest{Module: "captured", JSON: string(raw)}) - if err != nil { - b.Fatal(err) - } - key, err := delivery.ManifestsKey(digest) - if err != nil { - b.Fatal(err) - } - put(ManifestsRead, key, v) - } - builds, _ := filepath.Glob(filepath.Join(dir, "builds", "*.json")) - for _, f := range builds { - var bb struct { - Module string `json:"module"` - Fingerprint string `json:"fingerprint"` - Build json.RawMessage `json:"build"` - } - read(strings.TrimPrefix(f, dir+"/"), &bb) - key, err := delivery.BuilderBuildKey(bb.Module, bb.Fingerprint) - if err != nil { - b.Fatal(err) - } - put(BuildsRead, key, bb.Build) - } - archives := map[string][]byte{} - files, _ := filepath.Glob(filepath.Join(dir, "archives", "*")) - for _, f := range files { - raw, err := os.ReadFile(f) - if err != nil { - b.Fatal(err) - } - archives[filepath.Base(f)] = raw - } - return states, archives -} - -func BenchmarkComposeNode(b *testing.B) { - dir := os.Getenv("MESH_COMPOSE_CAPTURE") - if dir == "" { - b.Skip("MESH_COMPOSE_CAPTURE names no captured mesh") - } - node := os.Getenv("MESH_COMPOSE_NODE") - if node == "" { - node = "novox" - } - states, archives := captured(b, dir) - ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error { - for k, v := range states[state] { - if err := handle(stdio.StateChange{State: state, Key: k, Op: stdio.OpPut, Value: v, Current: true}); err != nil { - return err - } - } - return nil - }, func(state, key string) ([]byte, bool, error) { - v, ok := states[state][key] - return v, ok, nil - }) - fetch := fromruntime.KeptFetch(func(_ context.Context, url string) ([]byte, error) { - _, hex, _ := strings.Cut(url, "sha256:") - raw, ok := archives[hex] - if !ok { - return nil, os.ErrNotExist - } - return raw, nil - }) - own := &countingStore{Store: newStore()} - h := &Holder{Store: Watching(own, ws), Compose: composerOver(frombus.StatesReader(fromruntime.Watched{WatchedStates: ws}, fetch)), - Now: time.Now, Logf: func(string, ...any) {}} - // Composed twice before it is timed: a provision's state follows the states of those it depends on, which the first - // composition writes, so the second may still change a key; from then on nothing changes. - first, err := h.ComposeNode(context.Background(), node) - if err != nil { - b.Fatal(err) - } - if second, err := h.ComposeNode(context.Background(), node); err != nil { - b.Fatal(err) - } else if len(second.Changed) > 0 { - b.Logf("the second composition of %s changed %v", node, second.Changed) - } - b.Logf("first composition of %s: %d plans written, %d held, %d problems, %d keys changed", node, len(first.Plans), - len(first.Held), len(first.Problems), len(first.Changed)) - for _, p := range first.Problems { - b.Logf("problem: %s", p) - } - own.gets.Store(0) - own.keys.Store(0) - b.ResetTimer() - var c Composed - for b.Loop() { - c, err = h.ComposeNode(context.Background(), node) - if err != nil { - b.Fatal(err) - } - if len(c.Plans)+len(c.Gone)+len(c.Changed) > 0 { - b.Fatalf("an unchanged node composed changes: %+v", c) - } - } - b.Logf("the last composition of %s: %s", node, TookSaid(c.Took)) - b.ReportMetric(float64(own.gets.Load())/float64(b.N), "own-gets/op") - b.ReportMetric(float64(own.keys.Load())/float64(b.N), "own-keys/op") -} diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/composed_test.go b/modules/mesh-provisioner/cmd/mesh-provisioner/composed_test.go new file mode 100644 index 00000000..88eeaafd --- /dev/null +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/composed_test.go @@ -0,0 +1,204 @@ +package main + +// The provisioner makes every plan from the provisions the node-engine's published compositions carry (novox/hq ADR +// 0593 §6, §8), with the plans and provisions it wrote when it composed them itself. +// +// The fixtures were captured on 2026-11-02 from the holder as it stood before (frombus.PlansHeldTwice over mesh-sdk's +// frombustest.Provisioned mesh: restic on n1 requiring postgres-database from postgres there, the vault on n2): +// testdata/today-composed.json is what that composition answered at the node's wants and at its newest builds, +// testdata/today-states.golden.json what the holder then wrote. "ahead-" has restic wanted at an older build +// that requires nothing, so its key is ahead of its want and carries its plan entry (issue 1126 review 1). + +import ( + "context" + "encoding/json" + "os" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// today is a captured composition: the node, the builds its wants named, and the plans at them and at the newest. +type today struct { + Node string `json:"node"` + KeptAt map[string]string `json:"kept-at"` + AtWants struct { + Plans map[string]catalogue.Plan `json:"Plans"` + Held map[string]string `json:"Held"` + } `json:"at-wants"` + Newest struct { + Plans map[string]catalogue.Plan `json:"Plans"` + } `json:"newest"` +} + +// fpsOf is frombustest's modules and the fingerprints they are built at (frombustest.FPs). +var fpsOf = map[string]string{"restic": "src2:" + strings.Repeat("3", 64), "mesh-provisioner-client": "src2:" + strings.Repeat("4", 64), + "store": "src2:" + strings.Repeat("5", 64), "postgres": "src2:" + strings.Repeat("8", 64)} + +// published puts on the store what n1's engine publishes for the captured case: each placed module at its newest done +// build, and each wanted one at its want, each key carrying its plan. +func published(t *testing.T, s *memStore, c today) { + t.Helper() + put := func(module, fp string, plan catalogue.Plan, has bool) { + v := map[string]any{"v": 1, "node": c.Node, "module": module, "fp": fp, "resources": []any{}} + if has { + v["plan"] = plan + } + raw, _ := json.Marshal(v) + s.write(t, ComposedRead, mustKey(delivery.ComposedKey(c.Node, module, fp)), string(raw)) + } + for module, fp := range fpsOf { + plan, has := c.Newest.Plans[module] + put(module, fp, plan, has) + } + for module, fp := range c.KeptAt { + plan, has := c.AtWants.Plans[module] + put(module, fp, plan, has) + } + s.write(t, ComposedRead, mustKey(delivery.ComposedNodeKey(c.Node)), `{"v":1,"node":"`+c.Node+`","resources":[]}`) +} + +// goldenMesh is the store as the provisioner reads it for frombustest's mesh: the indexer's modules, n1's placement, the +// vault's on n2, restic's want. +func goldenMesh(t *testing.T, c today) *memStore { + t.Helper() + s := newStore() + for module, fp := range fpsOf { + // A provider's manifest says whether it keeps entries: postgres's, as frombustest has it, keeps none. + raw := `{"module":"` + module + `","version":"1"}` + if module == "postgres" { + raw = `{"module":"postgres","version":"1","provides":[{"name":"postgres-database","scope":"mesh"}],` + + `"serves":{"postgres-database":{"port":5432}}}` + } + digest := delivery.ManifestDigest([]byte(raw)) + value, _ := delivery.EncodeManifest(delivery.Manifest{Module: module, JSON: raw}) + s.write(t, ManifestsRead, mustKey(delivery.ManifestsKey(digest)), string(value)) + s.write(t, ModulesRead, module, `{"v":1,"fp2":"`+fp+`","manifest":"`+digest+`"}`) + } + s.write(t, PlacementRead, "placement."+c.Node, `{"v":1,"node":"`+c.Node+`","decided":{},"modules":[{"module":"store"},`+ + `{"module":"postgres"},{"module":"mesh-provisioner-client"},{"module":"restic"}]}`) + s.write(t, PlacementRead, "placement.n2", `{"v":1,"node":"n2","decided":{},"modules":[{"module":"mesh-vault"}]}`) + s.write(t, WantsRead, "want."+c.Node+".restic", `{"v":1,"fp":"`+c.KeptAt["restic"]+`","self":true}`) + return s +} + +// The plans and provisions made from the published compositions are today's, value for value, with a key ahead of its +// want carrying its plan entry; a pass over the same compositions writes nothing. +func TestThePlansFromCompositionsAreTodays(t *testing.T) { + for _, name := range []string{"", "ahead-"} { + t.Run("case "+name, func(t *testing.T) { + var c today + readJSON(t, "testdata/"+name+"today-composed.json", &c) + var golden map[string]map[string]json.RawMessage + readJSON(t, "testdata/"+name+"today-states.golden.json", &golden) + s := goldenMesh(t, c) + published(t, s, c) + now := time.Date(2026, 11, 2, 18, 0, 0, 0, time.UTC) + h := &Holder{Store: s, Asks: &router{}, Logf: t.Logf, Now: func() time.Time { return now }} + h.Compose = FromCompositions(s) + out, err := h.ComposeNode(context.Background(), c.Node) + if err != nil { + t.Fatal(err) + } + if len(out.Problems)+len(out.Held) > 0 { + t.Errorf("problems %v, held %v", out.Problems, out.Held) + } + for state, want := range golden { + keys, _ := s.Keys(state) + if len(keys) != len(want) { + t.Errorf("%s holds %v, today %d keys", state, keys, len(want)) + } + for key, raw := range want { + e, _ := s.Get(state, key) + if e == nil || !sameBytes(t, e.Value, raw) { + t.Errorf("%s %s:\n got %s\nwant %s", state, key, valueOf(e), raw) + } + } + } + again, err := h.ComposeNode(context.Background(), c.Node) + if err != nil || len(again.Plans)+len(again.Gone)+len(again.Changed) > 0 { + t.Errorf("a second pass over the same compositions wrote %+v (%v)", again, err) + } + }) + } +} + +// A composition the engine refused at the wanted build holds its module, its last plan kept and its words in `refused`; +// one the engine has not published yet holds it too, saying so; a module whose newest build is refused keeps its +// plan at its want. +func TestARefusedOrMissingCompositionHoldsItsModule(t *testing.T) { + var c today + readJSON(t, "testdata/today-composed.json", &c) + s := goldenMesh(t, c) + published(t, s, c) + h := &Holder{Store: s, Asks: &router{}, Logf: t.Logf, Now: time.Now} + h.Compose = FromCompositions(s) + if _, err := h.ComposeNode(context.Background(), c.Node); err != nil { + t.Fatal(err) + } + plan, _ := s.Get(PlansState, "n1.restic") + key := mustKey(delivery.ComposedKey("n1", "restic", fpsOf["restic"])) + s.write(t, ComposedRead, key, `{"v":1,"node":"n1","module":"restic","fp":"`+fpsOf["restic"]+`","resources":[],`+ + `"refused":"restic requires postgres-database and no module offers it"}`) + out, err := h.ComposeNode(context.Background(), c.Node) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out.Held["restic"], "no module offers it") { + t.Errorf("the refused module is held as %v", out.Held) + } + if e, _ := s.Get(PlansState, "n1.restic"); e == nil || string(e.Value) != string(plan.Value) { + t.Errorf("the held module's plan was not kept: %s", valueOf(e)) + } + if e, _ := s.Get(RefusedState, "n1.restic"); e == nil || !strings.Contains(string(e.Value), "no module offers it") { + t.Errorf("refused says %s", valueOf(e)) + } + if p, _ := s.Get(ProvisionsState, "n1.restic.postgres-database"); p == nil || strings.Contains(string(p.Value), "retiring") { + t.Errorf("the held module's key: %s", valueOf(p)) + } + // Not published yet: the want moved to a build the engine has not composed. + other := "src2:" + strings.Repeat("9", 64) + s.write(t, WantsRead, "want.n1.restic", `{"v":1,"fp":"`+other+`","self":true}`) + out, err = h.ComposeNode(context.Background(), c.Node) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out.Held["restic"], "not published") { + t.Errorf("the unpublished composition holds restic as %v", out.Held) + } + if e, _ := s.Get(PlansState, "n1.restic"); e == nil || string(e.Value) != string(plan.Value) { + t.Errorf("the plan was not kept while unpublished: %s", valueOf(e)) + } +} + +func readJSON(t *testing.T, path string, v any) { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(raw, v); err != nil { + t.Fatal(err) + } +} + +func sameBytes(t *testing.T, a, b []byte) bool { + t.Helper() + var x, y any + if json.Unmarshal(a, &x) != nil || json.Unmarshal(b, &y) != nil { + return false + } + ra, _ := json.Marshal(x) + rb, _ := json.Marshal(y) + return string(ra) == string(rb) +} + +func valueOf(e *Entry) string { + if e == nil { + return "(none)" + } + return string(e.Value) +} diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go b/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go index fb00bf2b..196855de 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go @@ -1,14 +1,16 @@ package main -// The seat's holder (novox/hq ADR 0570 clauses 1 to 5, 7; issue 1954 H1-H3): mesh-provisioner composes every placed -// module's provisioning plan and every provision from bus state, on change events and never on a timer, and keeps them -// in its states `plans` (`.`) and `provisions` (`..[.]`), which it alone -// writes. +// The seat's holder (novox/hq ADR 0570 clauses 1 to 5, 7, as changed by ADR 0593 §8; issue 1954 H1-H3): mesh-provisioner +// makes every placed module's provisioning plan and every provision from bus state, on change events and never on a +// timer, and keeps them in its states `plans` (`.`) and `provisions` +// (`..[.]`), which it alone writes. It composes nothing: each plan is the one the +// node-engine's published composition carries (`composed-`, ADR 0593 §6), the node-engine alone composing. // -// - **Composed per node.** A change of a node's placement or of one of its wants composes that node; a change of the -// indexer's module composes the nodes the module is placed on. Each node is composed twice: at the builds its wants -// name (its plans, what its client applies) and at the newest build of each placed module, so a build that adds a -// requirement has its key before its want (clause 2). A provision is needed while either needs it (clause 5). +// - **Made per node.** A change of a node's placement, of one of its wants or of one of its published compositions +// makes that node's plans again; a change of the indexer's module does it for the nodes the module is placed on. +// Each node's plans are read twice: at the builds its wants name (its plans, what its client applies) and at the +// newest build of each placed module, so a build that adds a requirement has its key before its want (clause 2). +// A provision is needed while either needs it (clause 5). // - **A key's state comes from its provider's outcome alone** (clause 3, 7): the provider's own entry for that // consumer, `_bound` under the same key: present is provided (the vault's generation carried into the // reference, clause 6), refused is failed with why, waits is waits with what. The holder never sets provided by @@ -32,8 +34,6 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/compose/frombus" - "git.novox.be/novox/mesh-sdk/go/compose/fromruntime" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" ) @@ -54,15 +54,15 @@ type Refusal struct { Since time.Time `json:"since"` } -// The reads it composes from. +// The reads it makes the plans from. ComposedRead is every node's published compositions, `composed-` (ADR 0593 +// §6), read as the node-engine's named state: its keys are `..` and `.node`. const ( PlacementRead = delivery.PlacementRead WantsRead = delivery.AssignerModule + ".wants" ModulesRead = delivery.IndexerModule + ".modules" ManifestsRead = delivery.IndexerModule + ".manifests" - BuildsRead = delivery.BuilderModule + ".builds" HaveRead = "node-engine.have" - ResolvedRead = "mesh-config.resolved" + ComposedRead = "mesh-host.composed" ) // BoundReads are the providers' entries this module's manifest reads, `.bound`, whose changes it hears. Which @@ -75,17 +75,29 @@ var BoundReads = []string{"gitea", "keycloak", "postgres", "route-adapter", "rou // boundRead is a provider's entries as this module reads them. func boundRead(module string) string { return module + ".bound" } -// Composer is a node's provisioning plans, composed from the bus over one read: at the builds keptAt names (module → -// fingerprint, the newest build of every other placed module), and at the newest build of each placed module (issue -// 1954: once, not twice). A module whose build or plan refuses is held (frombus.PlansHeld; issue 774, one bad module -// must not drop its node): in held with why, none of its plan in plans; a scope's Err is a refusal that is no single -// module's. +// Composer is a node's provisioning plans: at the builds keptAt names (module → fingerprint, the newest build of every +// other placed module), and at the newest build of each placed module. A module whose composition is refused, or not +// published yet, is held (issue 774, one bad module must not drop its node): in held with why, none of its plan in +// plans; a scope's Err is a failure that is no single module's. type Composer func(ctx context.Context, node string, keptAt map[string]string) Composition // Composition is a node's plans at its wanted builds and at its newest, and where the time went. type Composition struct { - AtWants, Newest frombus.HeldPlans - Took []frombus.Phase + AtWants, Newest HeldPlans + Took []Phase +} + +// HeldPlans is one scope's plans by module, the modules held with why, and a failure that is no single module's. +type HeldPlans struct { + Plans map[string]catalogue.Plan + Held map[string]string + Err error +} + +// Phase is one step of reading a node's plans and how long it took. +type Phase struct { + Name string + Took time.Duration } // Asker is the operator's channel as this module asks on it: publish an ask under its own name, and read the router's @@ -1106,25 +1118,139 @@ func orSaid(words ...string) string { return "" } -// FromWatched is the holder's composer over one watched copy of its reads (novox/hq ADR 0580 clause 2): frombus's plans -// of a node, at the builds keptAt names and at the newest over one read of the copy (placement, haves, modules, -// resolved values), a build by key and a manifest once per digest; no state is read key by key per composition. A plan -// names no value, so no own secret and no credential is read (compose.Planning). -func FromWatched(ws *stdio.WatchedStates) Composer { - return composerOver(fromruntime.WatchedReader(ws)) -} - -// composerOver is the holder's composer over a reader: both scopes over one read (frombus.PlansHeldTwice). -func composerOver(r frombus.Reader) Composer { - return func(ctx context.Context, node string, keptAt map[string]string) Composition { - at, newest, took := frombus.PlansHeldTwice(ctx, r, frombus.Mesh{}, frombus.Scope{Node: node, KeptAt: keptAt}) - return Composition{AtWants: at, Newest: newest, Took: took} +// FromCompositions is the holder's composer over the node-engine's published compositions (ADR 0593 §8): it composes +// nothing, and takes each module's plan from the key of `composed-` at the build it is read at. At the wanted +// builds a module is read at the fingerprint keptAt names, else at the indexer's newest (FP2); at the newest, each placed +// module at FP2. A key whose engine refused the module holds it with the engine's words; a key not published yet holds +// it at its wanted build (its last plan kept, as for a refusal) and leaves it out of the newest, whose build the engine +// writes once it is done (ADR 0593 §6). A plan absent from a key is a module that hands the provisioner nothing. +func FromCompositions(s Store) Composer { + return func(_ context.Context, node string, keptAt map[string]string) Composition { + start := time.Now() + at, newest := HeldPlans{Plans: map[string]catalogue.Plan{}, Held: map[string]string{}}, + HeldPlans{Plans: map[string]catalogue.Plan{}, Held: map[string]string{}} + published, err := publishedOn(s, node) + if err != nil { + at.Err, newest.Err = err, err + return Composition{AtWants: at, Newest: newest, Took: []Phase{{Name: "compositions read", Took: time.Since(start)}}} + } + placed, fp2 := placedModules(s, node) + atWants := map[string]string{} + for _, m := range placed { + atWants[m] = fp2[m] + } + for m, fp := range keptAt { + atWants[m] = fp + } + for _, m := range sortedKeys(atWants) { + fp := atWants[m] + if fp == "" { + continue + } + c, ok := published[composedAt(node, m, fp)] + switch { + case !ok: + at.Held[m] = fmt.Sprintf("the composition of %s at %s is not published by the node-engine of %s yet", m, fp, node) + case c.Refused != "": + at.Held[m] = c.Refused + case c.Plan != nil: + at.Plans[m] = *c.Plan + } + } + for _, m := range placed { + if fp2[m] == "" { + continue + } + c, ok := published[composedAt(node, m, fp2[m])] + switch { + case !ok: + case c.Refused != "": + newest.Held[m] = c.Refused + case c.Plan != nil: + newest.Plans[m] = *c.Plan + } + } + return Composition{AtWants: at, Newest: newest, Took: []Phase{{Name: "compositions read", Took: time.Since(start)}}} } } -// WatchedReads are the states the holder reads whole, each from the watched copy: what the composer reads and the -// wants (ADR 0580 clause 2), and the providers' entries, whose watch is also what rejudges a key. -var WatchedReads = append([]string{PlacementRead, WantsRead, ModulesRead, HaveRead, ResolvedRead}, boundReads()...) +// publishedPlan is what the holder reads of a published composition (delivery.Composed): its plan, or the engine's +// refusal. Read leniently, so a field the node-engine adds later is no failure here. +type publishedPlan struct { + Plan *catalogue.Plan `json:"plan"` + Refused string `json:"refused"` +} + +// composedAt is a module's key in its node's compositions, or "" for a fingerprint no key can name. +func composedAt(node, module, fp string) string { + k, err := delivery.ComposedKey(node, module, fp) + if err != nil { + return "" + } + return k +} + +// publishedOn is every module composition the node's engine publishes, by key. +func publishedOn(s Store, node string) (map[string]publishedPlan, error) { + keys, err := s.Keys(ComposedRead) + if err != nil { + return nil, fmt.Errorf("the compositions the node-engines publish cannot be read: %w", err) + } + out := map[string]publishedPlan{} + for _, k := range keys { + n, _, _, isNode, err := delivery.ParseComposedKey(k) + if err != nil || isNode || n != node { + continue + } + e, err := s.Get(ComposedRead, k) + if err != nil || e == nil { + continue + } + var c publishedPlan + if err := json.Unmarshal(e.Value, &c); err != nil { + c.Refused = fmt.Sprintf("its published composition %s does not read: %v", k, err) + } + out[k] = c + } + return out, nil +} + +// placedModules is every module placed on node, sorted, and the indexer's newest fingerprint (FP2) of each. +func placedModules(s Store, node string) ([]string, map[string]string) { + raw := map[string][]byte{} + if keys, err := s.Keys(PlacementRead); err == nil { + for _, k := range keys { + if n, _, _, err := delivery.ParsePlacementKey(k); err != nil || n != node { + continue + } + if e, err := s.Get(PlacementRead, k); err == nil && e != nil { + raw[k] = e.Value + } + } + } + placements, _ := delivery.PlacementsOf(raw) + var placed []string + fp2 := map[string]string{} + for _, m := range placements[node].Modules { + placed = append(placed, m.Module) + mk, err := delivery.ModulesKey(m.Module) + if err != nil { + continue + } + if e, err := s.Get(ModulesRead, mk); err == nil && e != nil { + if mod, err := delivery.DecodeModule(e.Value); err == nil { + fp2[m.Module] = mod.FP2 + } + } + } + sort.Strings(placed) + return placed, fp2 +} + +// WatchedReads are the states the holder reads whole, each from the watched copy (ADR 0580 clause 2): the placement, +// the wants, the indexer's modules, the haves and the published compositions, and the providers' entries, whose watch +// is also what rejudges a key. +var WatchedReads = append([]string{PlacementRead, WantsRead, ModulesRead, HaveRead, ComposedRead}, boundReads()...) // OwnStates are the states the holder alone writes (ADR 0570 clause 1), read from the watched copy too (issue 1954: some // 250 to 400 reads by key per composition): each of its own writes is taken into the copy at once (WatchedStates.Wrote), @@ -1206,8 +1332,9 @@ func (w watchingStore) Delete(state, key string) error { return w.ws.Removed(state, key) } -// NodesOfNodeKey is the node a key that starts with it names: a resolved value's `..`, a have's -// `.` or `.node`. A change of either recomposes that node (ADR 0580 clause 2). +// NodesOfNodeKey is the node a key that starts with it names: a have's `.` or `.node`, a published +// composition's `..` or `.node`. A change of either makes that node's plans +// again (ADR 0580 clause 2, ADR 0593 §8). func NodesOfNodeKey(key string) []string { if n, _, ok := strings.Cut(key, "."); ok && n != "" { return []string{n} diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/holder_integration_test.go b/modules/mesh-provisioner/cmd/mesh-provisioner/holder_integration_test.go index e5a56156..6ab8f93e 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/holder_integration_test.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/holder_integration_test.go @@ -2,12 +2,13 @@ package main -// The holder against a real bus (novox/hq ADR 0570 clauses 1-3, 5; issue 1954 H5): its states and every input it reads -// are a real nats-server's key-value buckets, its plans composed by mesh-sdk's composition from the watched copy of those -// buckets (ADR 0580 clause 2) filled with the mesh frombustest gives (restic on n1, requiring postgres-database from -// postgres there); no input is read key by key per composition. A placement and a -// want compose the key; the providers' outcomes (the vault's entry for the credential, postgres's own have) turn it -// provided; restic leaving the node makes it retiring and raises the ask. +// The holder against a real bus (novox/hq ADR 0570 clauses 1-3, 5, as changed by ADR 0593 §8; issue 1954 H5): its +// states and every input it reads are a real nats-server's key-value buckets, read through the watched copy of those +// buckets (ADR 0580 clause 2); the plans come from n1's published compositions in `composed-n1` (ADR 0593 §6), as +// captured from the composition of mesh-sdk's frombustest mesh (restic on n1, requiring postgres-database from postgres +// there), and nothing is composed. A placement, a want and a published composition make the key; the providers' +// outcomes (the vault's entry for the credential, postgres's own have) turn it provided; restic leaving the node makes +// it retiring and raises the ask. import ( "context" @@ -22,9 +23,6 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/asks" - "git.novox.be/novox/mesh-sdk/go/compose/frombus" - "git.novox.be/novox/mesh-sdk/go/compose/frombus/frombustest" - "git.novox.be/novox/mesh-sdk/go/compose/fromruntime" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-sdk/go/testbus" @@ -37,6 +35,9 @@ type kvStore struct { } func bucketOf(state string) string { + if state == ComposedRead { // the node-engine's named state: here n1's bucket, the one node that composes + return delivery.ComposedBucketPrefix + "n1" + } if owner, name, ok := strings.Cut(state, "."); ok { return owner + "_" + name } @@ -109,7 +110,7 @@ func (a *asked) Publish(_ string, body any, id string) error { func (a *asked) Record(string) ([]byte, bool, error) { return nil, false, nil } // 1954/H5. -func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) { +func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) nc, err := nats.Connect(testbus.URL(t)) @@ -122,85 +123,26 @@ func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) { t.Fatal(err) } s := kvStore{ctx: ctx, js: js} - mesh, err := frombustest.Provisioned() - if err != nil { - t.Fatal(err) - } - node := frombustest.Node - // The bus as the holder reads it: the indexer's modules and manifests, a placement naming where the vault runs, the - // want of restic on n1. - for module, m := range mesh.Module { - m.V = 1 - raw, _ := json.Marshal(m) - if err := s.Put(ModulesRead, module, json.RawMessage(raw)); err != nil { - t.Fatal(err) - } - } - for digest, raw := range mesh.Manifests { - var named struct { - Module string `json:"module"` - } - _ = json.Unmarshal(raw, &named) - value, _ := delivery.EncodeManifest(delivery.Manifest{Module: named.Module, JSON: string(raw)}) - key, _ := delivery.ManifestsKey(digest) - if err := s.Put(ManifestsRead, key, json.RawMessage(value)); err != nil { - t.Fatal(err) - } - } - // The vault keeps its outcomes as entries: its manifest declares `bound`. - vaultManifest := `{"module":"mesh-vault","version":"1","state":["bound"]}` + var c today + readJSON(t, "testdata/today-composed.json", &c) + node := c.Node + // The bus as the holder reads it: the indexer's modules and manifests, n1's placement and the vault's on n2, the + // want of restic on n1, and what n1's engine published. + mem := goldenMesh(t, c) + published(t, mem, c) + vaultManifest := `{"module":"mesh-vault","version":"1","state":["bound"]}` // the vault keeps its outcomes as entries vaultDigest := delivery.ManifestDigest([]byte(vaultManifest)) vaultValue, _ := delivery.EncodeManifest(delivery.Manifest{Module: provisions.VaultModule, JSON: vaultManifest}) - vaultKey, _ := delivery.ManifestsKey(vaultDigest) - if err := s.Put(ManifestsRead, vaultKey, json.RawMessage(vaultValue)); err != nil { - t.Fatal(err) - } - if err := s.Put(ModulesRead, provisions.VaultModule, map[string]any{"v": 1, "none2": "the test's", "manifest": vaultDigest, - "repository": "novox/mesh-catalog", "commit": "c1"}); err != nil { - t.Fatal(err) - } - vault, _ := json.Marshal(map[string]any{"v": 1, "node": "n2", "decided": map[string]any{}, - "modules": []map[string]any{{"module": provisions.VaultModule}}}) - if err := s.Put(PlacementRead, "placement.n2", json.RawMessage(vault)); err != nil { - t.Fatal(err) - } - fp := frombustest.FPs()["restic"] - if err := s.Put(WantsRead, "want."+node+".restic", map[string]any{"v": 1, "fp": fp, "self": true}); err != nil { - t.Fatal(err) - } - // The rest of the mesh on the bus as its writers keep it: n1's placement, its record in its have, the builds. - putPlacement := func() { - raw, err := delivery.EncodePlacement(mesh.Placement[node]) - if err != nil { - t.Fatal(err) - } - if err := s.Put(PlacementRead, "placement."+node, json.RawMessage(raw)); err != nil { - t.Fatal(err) + mem.write(t, ManifestsRead, mustKey(delivery.ManifestsKey(vaultDigest)), string(vaultValue)) + mem.write(t, ModulesRead, provisions.VaultModule, `{"v":1,"none2":"the test's","manifest":"`+vaultDigest+`"}`) + for state, keys := range mem.states { + for key, raw := range keys { + if err := s.Put(state, key, json.RawMessage(raw)); err != nil { + t.Fatal(err) + } } } - putPlacement() - for _, n := range []string{node, "n2"} { // n2, where the vault runs, has its record too - rec := mesh.Record[node] - rec.Node = n - recRaw, err := delivery.EncodeHaveNode(delivery.HaveNode{Record: &rec}) - if err != nil { - t.Fatal(err) - } - if err := s.Put(HaveRead, mustKey(delivery.HaveNodeKey(n)), json.RawMessage(recRaw)); err != nil { - t.Fatal(err) - } - } - for at, b := range mesh.Builds { - module, fp, _ := strings.Cut(at, " ") - raw, err := delivery.EncodeBuilderBuild(*b) - if err != nil { - t.Fatal(err) - } - if err := s.Put(BuildsRead, mustKey(delivery.BuilderBuildKey(module, fp)), json.RawMessage(raw)); err != nil { - t.Fatal(err) - } - } - // The watched copy over the real buckets: each state by one watch, a build and a manifest by key. + // The watched copy over the real buckets: each state by one watch, a manifest by key. ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error { kv, err := s.kv(state) if err != nil { @@ -250,18 +192,9 @@ func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) { } } } - fetch := func(_ context.Context, url string) ([]byte, error) { - for digest, body := range mesh.Archives { - if strings.HasSuffix(url, digest) { - return body, nil - } - } - return nil, errors.New("no archive at " + url) - } - reader := frombus.StatesReader(fromruntime.Watched{WatchedStates: ws}, fetch) a := &asked{} - h := &Holder{Store: Watching(s, ws), Asks: a, Now: time.Now, Logf: t.Logf, - Compose: composerOver(reader)} + h := &Holder{Store: Watching(s, ws), Asks: a, Now: time.Now, Logf: t.Logf} + h.Compose = FromCompositions(h.Store) state := func(key string) provisions.Provision { e, _ := s.Get(ProvisionsState, key) if e == nil { @@ -286,7 +219,7 @@ func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) { t.Fatal(err) } } - if err := s.Put(HaveRead, node+".postgres", map[string]any{"v": 1, "fp": frombustest.FPs()["postgres"], "phase": "running", + if err := s.Put(HaveRead, node+".postgres", map[string]any{"v": 1, "fp": fpsOf["postgres"], "phase": "running", "health": "ok"}); err != nil { t.Fatal(err) } @@ -299,17 +232,11 @@ func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) { if p := state(key); p.State != provisions.Provided { t.Fatalf("the outcomes did not turn it provided: %+v", p) } - // restic leaves the node and its want goes: retiring, one ask. - p := mesh.Placement[node] - var kept []delivery.PlacedModule - for _, m := range p.Modules { - if m.Module != "restic" { - kept = append(kept, m) - } + // restic leaves the node and its want goes: retiring, one ask. Its compositions stay until its engine deletes them. + if err := s.Put(PlacementRead, "placement."+node, json.RawMessage(`{"v":1,"node":"`+node+`","decided":{},`+ + `"modules":[{"module":"store"},{"module":"postgres"},{"module":"mesh-provisioner-client"}]}`)); err != nil { + t.Fatal(err) } - p.Modules = kept - mesh.Placement[node] = p - putPlacement() if err := s.Delete(WantsRead, "want."+node+".restic"); err != nil { t.Fatal(err) } diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go b/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go index d737cc94..0ba670fc 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go @@ -15,7 +15,6 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/compose/frombus" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" ) @@ -71,8 +70,8 @@ func newMesh() *mesh { Now: func() time.Time { now = now.Add(time.Second); return now }, Compose: func(_ context.Context, node string, keptAt map[string]string) Composition { m.composed = append(m.composed, node) - return Composition{AtWants: frombus.HeldPlans{Plans: m.atWant[node], Held: m.heldAt[node]}, - Newest: frombus.HeldPlans{Plans: m.atNewest[node]}} + return Composition{AtWants: HeldPlans{Plans: m.atWant[node], Held: m.heldAt[node]}, + Newest: HeldPlans{Plans: m.atNewest[node]}} }} // What each provider's manifest declares: postgres, keycloak and the vault keep entries, the others report by their // have. @@ -386,10 +385,10 @@ func TestAModuleHeldWithoutAWantIsComposedAtTheBuildItRuns(t *testing.T) { var keptAt map[string]string m.h.Compose = func(_ context.Context, node string, at map[string]string) Composition { keptAt = at - return Composition{AtWants: frombus.HeldPlans{Plans: map[string]catalogue.Plan{"nats": {}}}} + return Composition{AtWants: HeldPlans{Plans: map[string]catalogue.Plan{"nats": {}}}} } // ADR 0580 clause 2: the reads come from one watched copy, each state watched once and none read key by key from the - // bus, and a change the copy takes wakes the node it touches (a resolved value its node). + // bus, and a change the copy takes wakes the node it touches (a published composition its node). bus := &readsOf{Store: m.s, read: map[string]int{}} handles, watches := map[string]func(stdio.StateChange) error{}, map[string]int{} ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error { @@ -404,7 +403,7 @@ func TestAModuleHeldWithoutAWantIsComposedAtTheBuildItRuns(t *testing.T) { }, func(string, string) ([]byte, bool, error) { return nil, false, nil }) m.h.Store = Watching(bus, ws) wk := &work{pending: map[string]bool{}, wake: make(chan struct{}, 1)} - for _, r := range []string{ResolvedRead, HaveRead} { + for _, r := range []string{ComposedRead, HaveRead} { ws.Follow(r, func(c stdio.StateChange) error { wk.add(NodesOfNodeKey(c.Key)...); return nil }) } for i := range 2 { @@ -429,12 +428,14 @@ func TestAModuleHeldWithoutAWantIsComposedAtTheBuildItRuns(t *testing.T) { t.Errorf("%s was read %d times from the bus and watched %d times", r, bus.read[r], watches[r]) } } - // The current haves queued their nodes when watched; a later change of a resolved value or a have queues its node. + // The current haves queued their nodes when watched; a later change of a published composition or a have + // queues its node. wk.pending = map[string]bool{} - _ = handles[ResolvedRead](stdio.StateChange{Key: "g14.dunst.font-size", Op: stdio.OpPut, Value: []byte("13")}) + _ = handles[ComposedRead](stdio.StateChange{Key: "g14.dunst." + strings.Repeat("ee", 32), Op: stdio.OpPut, + Value: []byte("{}")}) _ = handles[HaveRead](stdio.StateChange{Key: "ace.restic", Op: stdio.OpDelete}) if !wk.pending["g14"] || !wk.pending["ace"] || len(wk.pending) != 2 { - t.Errorf("a resolved value's and a have's change queued %v, want g14 and ace", wk.pending) + t.Errorf("a published composition's and a have's change queued %v, want g14 and ace", wk.pending) } } diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/holdertools.go b/modules/mesh-provisioner/cmd/mesh-provisioner/holdertools.go index 8690cd73..12b4c906 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/holdertools.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/holdertools.go @@ -1,7 +1,7 @@ package main // The holder's tools (novox/hq ADR 0570 Consequences, ADR 0564 §12's rule): a read of every key of `provisions` and -// `plans`, one key whole, each state's fill, composing one node now, and reading the operator's answers now. +// `plans`, one key whole, each state's fill, making one node's plans now, and reading the operator's answers now. import ( "context" @@ -36,9 +36,10 @@ func holderTools(h *Holder) []stdio.Tool { return h.read(ProvisionsState, arg(a, "key"), arg(a, "node"), arg(a, "state")) }}, {Name: PlansTool, - Description: "Every module's provisioning plan the seat composed, ., narrowed by node, with how many " + - "keys and bytes the state holds, and each placed module held with no plan composed and why (refused: its " + - "manifest or its plan refuses, the node's other modules composed); or one plan whole.", + Description: "Every module's provisioning plan the seat keeps, ., as the node-engine's published " + + "composition carries it (composed-), narrowed by node, with how many keys and bytes the state holds, " + + "and each placed module held with its last plan kept and why (refused: the node-engine refused its " + + "composition, or has not published it at the wanted build yet); or one plan whole.", Input: map[string]any{"type": "object", "properties": map[string]any{"key": str("one plan, ."), "node": str("a node")}}, Run: func(a map[string]any) (any, error) { @@ -54,8 +55,9 @@ func holderTools(h *Holder) []stdio.Tool { return out, nil }}, {Name: ComposeTool, - Description: "Compose one node's plans and provisions now from the bus, as a change event would, and say what " + - "was written, removed, changed and asked.", + Description: "Make one node's plans and provisions now from the node-engine's published compositions and the " + + "providers' outcomes, as a change event would (it composes nothing), and say what was written, removed, " + + "changed and asked, and where the time went.", Input: map[string]any{"type": "object", "properties": map[string]any{"node": str("the node")}, "required": []string{"node"}}, Run: func(a map[string]any) (any, error) { diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/main.go b/modules/mesh-provisioner/cmd/mesh-provisioner/main.go index 33d50c83..ad007f0d 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/main.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/main.go @@ -1,7 +1,7 @@ // mesh-provisioner: the provisioner's server, holder of the mesh-provisioner seat once for the mesh (novox/hq ADR // 0349 clause 3, ADR 0570). A Go bundle served by a runtime of its own, on its own bus account (`runs-as`, ADR 0259 -// §8), keeping mesh-provisioner_provisions and _plans (every provision and every plan, composed from the bus on change -// events, its alone), _registry (which domain answers which kind of placeholder, its alone), and _applied and +// §8), keeping mesh-provisioner_provisions and _plans (every provision and every plan, made from the node-engines' +// published compositions on change events, its alone; novox/hq ADR 0593 §8), _registry (which domain answers which kind of placeholder, its alone), and _applied and // _collect, which each machine's provisioner client writes for its own machine; _needs is retired and emptied at its // start. stdout is the MCP channel; what this module says, it says on stderr. package main @@ -125,14 +125,15 @@ func slowest(took []Took) time.Duration { func main() { s := runtimeStore{} ws := stdio.NewWatchedStates() // one copy of what it reads, each state by its watch (ADR 0580 clause 2) - h := &Holder{Store: Watching(s, ws), Compose: FromWatched(ws), Asks: runtimeAsks{}, Now: time.Now, Logf: logf} + h := &Holder{Store: Watching(s, ws), Asks: runtimeAsks{}, Now: time.Now, Logf: logf} + h.Compose = FromCompositions(h.Store) // the node-engines' published compositions, from the copy (ADR 0593 §8) w := &work{pending: map[string]bool{}, wake: make(chan struct{}, 1)} go w.run(h) go starting(h) // Each watch's handler runs after the copy took its change, so a composition it wakes reads the copy that change made. go ws.Follow(PlacementRead, func(c stdio.StateChange) error { w.add(NodesOfPlacement(c.Key)...); return nil }) go ws.Follow(WantsRead, func(c stdio.StateChange) error { w.add(NodesOfWant(c.Key)...); return nil }) - go ws.Follow(ResolvedRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil }) + go ws.Follow(ComposedRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil }) go ws.Follow(HaveRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil }) go ws.Follow(ModulesRead, func(c stdio.StateChange) error { if module, err := delivery.ParseModulesKey(c.Key); err == nil { diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/provisioner_test.go b/modules/mesh-provisioner/cmd/mesh-provisioner/provisioner_test.go index f717ea06..7ef2233a 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/provisioner_test.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/provisioner_test.go @@ -9,7 +9,7 @@ import ( "sync" "testing" - "git.novox.be/novox/mesh-sdk/go/compose/frombus" + "git.novox.be/novox/mesh-sdk/go/delivery" ) // memStore is the bus as mesh-provisioner reaches it, in memory: the states it keeps. @@ -326,9 +326,15 @@ func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) { t.Errorf("the manifest lists %s, which is not served", tool) } } - for _, r := range append([]string{WantsRead}, frombus.StatesReads...) { // what fromruntime.Reader reads + for _, r := range append(slices.Clone(WatchedReads), ManifestsRead) { if !slices.Contains(m.Reads, r) { - t.Errorf("the manifest does not read %s, which the composition reads", r) + t.Errorf("the manifest does not read %s, which the holder reads", r) + } + } + // It composes nothing (ADR 0593 §8): no build, and no config value, is read to make a plan. + for _, r := range []string{delivery.BuilderModule + ".builds", "mesh-config.resolved"} { + if slices.Contains(m.Reads, r) { + t.Errorf("the manifest reads %s, which only a composer reads", r) } } for _, p := range BoundReads { diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/ahead-today-composed.json b/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/ahead-today-composed.json new file mode 100644 index 00000000..a2275e76 --- /dev/null +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/ahead-today-composed.json @@ -0,0 +1,100 @@ +{ + "node": "n1", + "kept-at": { + "restic": "src2:2222222222222222222222222222222222222222222222222222222222222222" + }, + "at-wants": { + "Plans": { + "restic": { + "templates": { + "source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e", + "digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e" + }, + "files": [ + { + "id": "declared", + "path": "/var/lib/restic/backups.conf", + "template": "backups.conf", + "owner-at-runtime": "owned" + }, + { + "id": "data-declared", + "path": "/var/lib/restic/data.conf", + "template": "data.conf", + "owner-at-runtime": "owned" + }, + { + "id": "retention", + "path": "/var/lib/restic/retention.conf", + "template": "retention.conf", + "owner-at-runtime": "owned" + } + ], + "placement": { + "backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n", + "backup:lines": "" + }, + "secrets": [ + { + "name": "repository", + "path": "/var/lib/restic/repository.secret" + } + ] + } + }, + "Held": {}, + "Err": null + }, + "newest": { + "Plans": { + "restic": { + "templates": { + "source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e", + "digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e" + }, + "files": [ + { + "id": "declared", + "path": "/var/lib/restic/backups.conf", + "template": "backups.conf", + "owner-at-runtime": "owned" + }, + { + "id": "data-declared", + "path": "/var/lib/restic/data.conf", + "template": "data.conf", + "owner-at-runtime": "owned" + }, + { + "id": "retention", + "path": "/var/lib/restic/retention.conf", + "template": "retention.conf", + "owner-at-runtime": "owned" + } + ], + "placement": { + "backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n", + "backup:lines": "" + }, + "secrets": [ + { + "name": "repository", + "path": "/var/lib/restic/repository.secret" + } + ], + "provisions": [ + { + "provision": "postgres-database", + "provider": "n1", + "provider-module": "postgres", + "at": "127.0.0.1", + "from": "n1", + "as": "mesh_n1_restic" + } + ] + } + }, + "Held": {}, + "Err": null + } +} diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/ahead-today-states.golden.json b/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/ahead-today-states.golden.json new file mode 100644 index 00000000..dd17dcc2 --- /dev/null +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/ahead-today-states.golden.json @@ -0,0 +1,71 @@ +{ + "plans": { + "n1.restic": { + "v": 1, + "node": "n1", + "module": "restic", + "plan": { + "templates": { + "source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e", + "digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e" + }, + "files": [ + { + "id": "declared", + "path": "/var/lib/restic/backups.conf", + "template": "backups.conf", + "owner-at-runtime": "owned" + }, + { + "id": "data-declared", + "path": "/var/lib/restic/data.conf", + "template": "data.conf", + "owner-at-runtime": "owned" + }, + { + "id": "retention", + "path": "/var/lib/restic/retention.conf", + "template": "retention.conf", + "owner-at-runtime": "owned" + } + ], + "placement": { + "backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n", + "backup:lines": "" + }, + "secrets": [ + { + "name": "repository", + "path": "/var/lib/restic/repository.secret" + } + ] + } + } + }, + "provisions": { + "n1.restic.postgres-database": { + "v": 1, + "provision": "postgres-database", + "consumer": { + "module": "restic", + "node": "n1" + }, + "provider": { + "module": "postgres", + "node": "n1" + }, + "state": "asked", + "why": "no plan names it yet: the newest build of restic requires it and its wanted build does not, so its provider binds it from this key's plan entry; waits for postgres on n1 to run: it says nothing yet", + "plan": { + "provision": "postgres-database", + "provider": "n1", + "provider-module": "postgres", + "at": "127.0.0.1", + "from": "n1", + "as": "mesh_n1_restic" + }, + "at": "2026-11-02T18:00:00Z" + } + }, + "refused": {} +} diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/today-composed.json b/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/today-composed.json new file mode 100644 index 00000000..537b17a8 --- /dev/null +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/today-composed.json @@ -0,0 +1,110 @@ +{ + "node": "n1", + "kept-at": { + "restic": "src2:3333333333333333333333333333333333333333333333333333333333333333" + }, + "at-wants": { + "Plans": { + "restic": { + "templates": { + "source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e", + "digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e" + }, + "files": [ + { + "id": "declared", + "path": "/var/lib/restic/backups.conf", + "template": "backups.conf", + "owner-at-runtime": "owned" + }, + { + "id": "data-declared", + "path": "/var/lib/restic/data.conf", + "template": "data.conf", + "owner-at-runtime": "owned" + }, + { + "id": "retention", + "path": "/var/lib/restic/retention.conf", + "template": "retention.conf", + "owner-at-runtime": "owned" + } + ], + "placement": { + "backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n", + "backup:lines": "" + }, + "secrets": [ + { + "name": "repository", + "path": "/var/lib/restic/repository.secret" + } + ], + "provisions": [ + { + "provision": "postgres-database", + "provider": "n1", + "provider-module": "postgres", + "at": "127.0.0.1", + "from": "n1", + "as": "mesh_n1_restic" + } + ] + } + }, + "Held": {}, + "Err": null + }, + "newest": { + "Plans": { + "restic": { + "templates": { + "source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e", + "digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e" + }, + "files": [ + { + "id": "declared", + "path": "/var/lib/restic/backups.conf", + "template": "backups.conf", + "owner-at-runtime": "owned" + }, + { + "id": "data-declared", + "path": "/var/lib/restic/data.conf", + "template": "data.conf", + "owner-at-runtime": "owned" + }, + { + "id": "retention", + "path": "/var/lib/restic/retention.conf", + "template": "retention.conf", + "owner-at-runtime": "owned" + } + ], + "placement": { + "backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n", + "backup:lines": "" + }, + "secrets": [ + { + "name": "repository", + "path": "/var/lib/restic/repository.secret" + } + ], + "provisions": [ + { + "provision": "postgres-database", + "provider": "n1", + "provider-module": "postgres", + "at": "127.0.0.1", + "from": "n1", + "as": "mesh_n1_restic" + } + ] + } + }, + "Held": {}, + "Err": null + } +} diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/today-states.golden.json b/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/today-states.golden.json new file mode 100644 index 00000000..0bd4fd51 --- /dev/null +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/testdata/today-states.golden.json @@ -0,0 +1,73 @@ +{ + "plans": { + "n1.restic": { + "v": 1, + "node": "n1", + "module": "restic", + "plan": { + "templates": { + "source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e", + "digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e" + }, + "files": [ + { + "id": "declared", + "path": "/var/lib/restic/backups.conf", + "template": "backups.conf", + "owner-at-runtime": "owned" + }, + { + "id": "data-declared", + "path": "/var/lib/restic/data.conf", + "template": "data.conf", + "owner-at-runtime": "owned" + }, + { + "id": "retention", + "path": "/var/lib/restic/retention.conf", + "template": "retention.conf", + "owner-at-runtime": "owned" + } + ], + "placement": { + "backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n", + "backup:lines": "" + }, + "secrets": [ + { + "name": "repository", + "path": "/var/lib/restic/repository.secret" + } + ], + "provisions": [ + { + "provision": "postgres-database", + "provider": "n1", + "provider-module": "postgres", + "at": "127.0.0.1", + "from": "n1", + "as": "mesh_n1_restic" + } + ] + } + } + }, + "provisions": { + "n1.restic.postgres-database": { + "v": 1, + "provision": "postgres-database", + "consumer": { + "module": "restic", + "node": "n1" + }, + "provider": { + "module": "postgres", + "node": "n1" + }, + "state": "asked", + "why": "waits for postgres on n1 to run: it says nothing yet", + "at": "2026-11-02T18:00:00Z" + } + }, + "refused": {} +} diff --git a/modules/mesh-provisioner/module.json b/modules/mesh-provisioner/module.json index b7e6d8a5..05a747a8 100644 --- a/modules/mesh-provisioner/module.json +++ b/modules/mesh-provisioner/module.json @@ -19,9 +19,8 @@ "mesh-delivery-assigner.wants", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.manifests", - "mesh-delivery-builder.builds", "node-engine.have", - "mesh-config.resolved", + "mesh-host.composed", "gitea.bound", "keycloak.bound", "postgres.bound", From 9c57f7afb339b4811ad5edffeb55a93e6675cbd0 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 18:22:41 +0100 Subject: [PATCH 02/16] =?UTF-8?q?mesh-build-go=20publishes=20the=20build?= =?UTF-8?q?=20runner=20as=20its=20own=20package,=20so=20the=20other=20buil?= =?UTF-8?q?d=20modules=20share=20it=20without=20mesh-sdk=20(ADR=200593=20?= =?UTF-8?q?=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