diff --git a/checks/controllerdeps/controllerdeps.go b/checks/controllerdeps/controllerdeps.go index 0d784fb0..db0e4a40 100644 --- a/checks/controllerdeps/controllerdeps.go +++ b/checks/controllerdeps/controllerdeps.go @@ -10,6 +10,9 @@ // (`mesh-controller.`, `seat:mesh-controller.`, `mesh-controller_`), anywhere in the manifest; // - a manifest string that is the bare module name `mesh-controller` outside the module's own `config`, where a value // is the module's data (an issue owner naming the controller's repository is no dependency), not an address; +// - a go.mod of a module or of a check requiring the controller's repository (its module path, github.com/novox/mesh-controller, +// or the forge's git.novox.be/novox/mesh-controller): the repository is archived once its last module is delivered +// elsewhere (novox/hq ADR 0593 §11), so nothing here may be built from it; // - a line of any module's file naming one of the controller's buckets (`mesh-controller_`): its buckets are removed // after the controller's end (ADR 0590 §6), so a reader of one reads nothing. A test (`_test.go`, `.test.ts`) reads // no live bucket, its names are its own data, and a `testdata` directory is a copy of text from elsewhere (a design, @@ -40,6 +43,9 @@ const ( // address is a string naming the controller's seat, one of its states, verbs or events, or one of its buckets. var address = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller([._][A-Za-z0-9])`) +// repository is a go.mod line naming the controller's repository by either of its module paths. +var repository = regexp.MustCompile(`(github\.com|git\.novox\.be)/novox/mesh-controller(/|\s|$)`) + // Refusals is each reason the check gives over the tree at root, sorted; none when it passes. func Refusals(root string) ([]string, error) { var out []string @@ -65,6 +71,26 @@ func Refusals(root string) ([]string, error) { "consumes, waits on or uses it; name the seat that holds the job now (novox/hq ADR 0590 §5)", module, s)) } } + mods, err := filepath.Glob(filepath.Join(root, "*", "*", "go.mod")) + if err != nil { + return nil, err + } + sort.Strings(mods) + for _, f := range mods { + if rel, _ := filepath.Rel(root, f); !strings.HasPrefix(rel, "modules"+string(filepath.Separator)) && !strings.HasPrefix(rel, "checks"+string(filepath.Separator)) { + continue + } + raw, err := os.ReadFile(f) + if err != nil { + return nil, err + } + for n, l := range strings.Split(string(raw), "\n") { + if repository.MatchString(l) { + rel, _ := filepath.Rel(root, f) + out = append(out, fmt.Sprintf("%s:%d: requires the controller's repository: it is archived once its last module is built elsewhere, so nothing here is built from it (novox/hq ADR 0593 §11)", filepath.ToSlash(rel), n+1)) + } + } + } buckets, err := bucketLines(root) if err != nil { return nil, err diff --git a/checks/controllerdeps/controllerdeps_test.go b/checks/controllerdeps/controllerdeps_test.go index 9a2e221a..0a05e0ae 100644 --- a/checks/controllerdeps/controllerdeps_test.go +++ b/checks/controllerdeps/controllerdeps_test.go @@ -33,6 +33,9 @@ func TestRefusalsJudgeATree(t *testing.T) { {"a requirement", map[string]string{"modules/d/module.json": `{"module":"d","requires":[{"name":"x","from":"mesh-controller"}]}`}, `d: names "mesh-controller"`}, {"the controller's own module", map[string]string{"modules/mesh-controller/module.json": `{"module":"mesh-controller"}`}, "mesh-controller: the controller is no module of the catalogue"}, {"a bucket in code", map[string]string{"modules/e/cmd/e/main.go": "package main\nconst b = \"mesh-controller_assignments\"\n"}, "modules/e/cmd/e/main.go:2: names the controller's bucket"}, + {"a requirement of the controller's repository", map[string]string{"modules/h/go.mod": "module h\n\nrequire github.com/novox/mesh-controller v0.0.0\n"}, "modules/h/go.mod:3: requires the controller's repository"}, + {"its forge path in a check", map[string]string{"checks/i/go.mod": "module i\n\nrequire (\n\tgit.novox.be/novox/mesh-controller/modules/route-proxy v0.1.0\n)\n"}, "checks/i/go.mod:4"}, + {"another repository of the forge passes", map[string]string{"modules/j/go.mod": "module j\n\nrequire git.novox.be/novox/mesh-sdk/go v0.1.0\n"}, ""}, {"a bucket in a test is its data", map[string]string{"modules/e/cmd/e/main_test.go": "package main\n// mesh-controller_lease\n"}, ""}, {"a fixture copied from elsewhere", map[string]string{"modules/e/cmd/e/testdata/x.md": "mesh-controller_rollouts\n"}, ""}, {"an owner and prose pass", map[string]string{"modules/f/module.json": `{"module":"f","config":{"owners":{"default":["mesh-controller","mesh-controller modules/route-proxy"]}},"why":"the mesh-controller module is gone"}`}, ""}, diff --git a/checks/credentialform/go.mod b/checks/credentialform/go.mod index 021ef82f..1fbbb379 100644 --- a/checks/credentialform/go.mod +++ b/checks/credentialform/go.mod @@ -2,4 +2,4 @@ module checks/credentialform go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 diff --git a/checks/credentialform/go.sum b/checks/credentialform/go.sum index 1126fe29..2effc70b 100644 --- a/checks/credentialform/go.sum +++ b/checks/credentialform/go.sum @@ -1,2 +1,2 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= diff --git a/checks/deps/main.go b/checks/deps/main.go index dae84e1e..702f35df 100644 --- a/checks/deps/main.go +++ b/checks/deps/main.go @@ -111,13 +111,24 @@ func forgeRequirements(dir string) ([]requirement, error) { if err != nil { return nil, fmt.Errorf("its go.mod cannot be read: %s", lastLine(out)) } - var mod struct{ Require []requirement } + var mod struct { + Require []requirement + Replace []struct{ Old, New requirement } + } if err := json.Unmarshal(out, &mod); err != nil { return nil, fmt.Errorf("its go.mod cannot be read: %w", err) } + // A requirement the go.mod replaces to a directory is a package of the module's own repository (novox/hq ADR 0593 §1): it + // is in the tree under check, so it is fetched from nowhere. + local := map[string]bool{} + for _, r := range mod.Replace { + if r.New.Version == "" { + local[r.Old.Path] = true + } + } var on []requirement for _, r := range mod.Require { - if strings.HasPrefix(r.Path, forge) { + if strings.HasPrefix(r.Path, forge) && !local[r.Path] { on = append(on, r) } } diff --git a/checks/deps/main_test.go b/checks/deps/main_test.go index 3328fbc4..047b2035 100644 --- a/checks/deps/main_test.go +++ b/checks/deps/main_test.go @@ -175,3 +175,28 @@ func TestTheRefOfAVersion(t *testing.T) { } } } + +// ADR 0593 §1: a module requires a sibling package of its own repository and replaces it to the directory beside it; that +// package is no fetch from the forge (it is in the tree under check), so it is never looked for in a clone beside the check, +// and the module builds with the SDK taken from there. +func TestASiblingPackageReplacedToItsDirectoryIsNotFetched(t *testing.T) { + offline(t) + beside, commit := sdkBeside(t) + version := "v0.1.14-0.20261012082113-" + commit[:12] + root := t.TempDir() + sibling := "git.novox.be/novox/mesh-catalog/modules/builder/runner" + write(t, filepath.Join(root, "builder", "runner", "go.mod"), "module "+sibling+"\n\ngo 1.22\n\nrequire git.novox.be/novox/mesh-sdk/go "+version+"\n") + write(t, filepath.Join(root, "builder", "runner", "run.go"), "package runner\n\nimport \"git.novox.be/novox/mesh-sdk/go/sdk\"\n\nfunc Run() string { return sdk.Hello() }\n") + dir := filepath.Join(root, "user") + write(t, filepath.Join(dir, "go.mod"), "module git.novox.be/novox/mesh-catalog/modules/user\n\ngo 1.22\n\nrequire (\n\t"+sibling+" v0.0.0\n"+ + "\tgit.novox.be/novox/mesh-sdk/go "+version+"\n)\n\nreplace "+sibling+" => ../builder/runner\n") + write(t, filepath.Join(dir, "main.go"), "package main\n\nimport \""+sibling+"\"\n\nfunc main() { println(runner.Run()) }\n") + modfile, err := resolve(dir, beside, t.TempDir()) + if err != nil { + t.Fatalf("resolve: %v", err) + } + if modfile == "" { + t.Fatal("resolve said the module fetches its dependencies, with nothing to fetch the SDK from") + } + builds(t, dir, modfile) +} diff --git a/checks/failurekit/failurekit.go b/checks/failurekit/failurekit.go index d95afa6a..9738d66a 100644 --- a/checks/failurekit/failurekit.go +++ b/checks/failurekit/failurekit.go @@ -1,4 +1,4 @@ -// Package failurekit refuses a build module whose tests do not run mesh-sdk's conformance kit (issue 1901, its thorough +// Package failurekit refuses a build module whose tests do not run the build runner's conformance kit (issue 1901, its thorough // fix): a module that claims a node-build-* seat and serves a test step must hold a test that calls conformance.Run, so // its parse of the test runner's output is held to the failure contract (a failing test named with its cause, never a // passing line in its place). A build module with no test step (mesh-build-image builds images and runs no tests) is @@ -71,7 +71,7 @@ func Judge(root string) ([]Judged, error) { } if !runs { refused = append(refused, fmt.Sprintf("it claims %s and serves the test step %s, and no test of it calls conformance.Run "+ - "(mesh-sdk go/buildstep/conformance): run the kit with the module's own parse path (issue 1901)", seat, test)) + "(mesh-build-go buildstep/conformance): run the kit with the module's own parse path (issue 1901)", seat, test)) } } // Every build module keeps steps, so every one runs the left-running case, test step or not (issue 1890). @@ -81,7 +81,7 @@ func Judge(root string) ([]Judged, error) { } if !sweeps { refused = append(refused, fmt.Sprintf("it claims %s, and no test of it calls conformance.LeftRunning "+ - "(mesh-sdk go/buildstep/conformance): run the kit's left-running case with the module's own start sweep (issue 1890)", seat)) + "(mesh-build-go buildstep/conformance): run the kit's left-running case with the module's own start sweep (issue 1890)", seat)) } j.Refused = strings.Join(refused, "; ") out = append(out, j) @@ -121,7 +121,7 @@ func runsTheKit(dir, call string) (bool, error) { return err } found = strings.Contains(string(raw), call) && - strings.Contains(string(raw), `"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"`) + strings.Contains(string(raw), `"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/conformance"`) return nil }) return found, err diff --git a/checks/failurekit/failurekit_test.go b/checks/failurekit/failurekit_test.go index f08ecf7b..17f6fce4 100644 --- a/checks/failurekit/failurekit_test.go +++ b/checks/failurekit/failurekit_test.go @@ -11,7 +11,7 @@ import ( // cases is refused, and a module that claims no node-build-* seat is not judged. func TestABuildModuleWithATestStepMustRunTheKit(t *testing.T) { root := t.TempDir() - const imp = "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/buildstep/conformance\"\n\n" + const imp = "package main\n\nimport \"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/conformance\"\n\n" const sweep = "func TestSweep(t *testing.T) { conformance.LeftRunning(t, nil) }\n" kit := imp + "func TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n" + sweep for path, body := range map[string]string{ diff --git a/checks/health/go.mod b/checks/health/go.mod index 15a37ef7..f25e878c 100644 --- a/checks/health/go.mod +++ b/checks/health/go.mod @@ -2,4 +2,4 @@ module checks/health go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 diff --git a/checks/health/go.sum b/checks/health/go.sum index 1126fe29..2effc70b 100644 --- a/checks/health/go.sum +++ b/checks/health/go.sum @@ -1,2 +1,2 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= diff --git a/checks/manifests/go.mod b/checks/manifests/go.mod index 5e224f2a..80aa33c7 100644 --- a/checks/manifests/go.mod +++ b/checks/manifests/go.mod @@ -2,4 +2,4 @@ module checks/manifests go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 diff --git a/checks/manifests/go.sum b/checks/manifests/go.sum index 1126fe29..2effc70b 100644 --- a/checks/manifests/go.sum +++ b/checks/manifests/go.sum @@ -1,2 +1,2 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= diff --git a/checks/readable/go.mod b/checks/readable/go.mod index c9ab7b7a..ff733241 100644 --- a/checks/readable/go.mod +++ b/checks/readable/go.mod @@ -2,4 +2,4 @@ module checks/readable go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 diff --git a/checks/readable/go.sum b/checks/readable/go.sum index 1126fe29..2effc70b 100644 --- a/checks/readable/go.sum +++ b/checks/readable/go.sum @@ -1,2 +1,2 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= diff --git a/checks/removedpackages/go.mod b/checks/removedpackages/go.mod new file mode 100644 index 00000000..be738a7f --- /dev/null +++ b/checks/removedpackages/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-catalog/checks/removedpackages + +go 1.22 diff --git a/checks/removedpackages/main.go b/checks/removedpackages/main.go new file mode 100644 index 00000000..d77f0d08 --- /dev/null +++ b/checks/removedpackages/main.go @@ -0,0 +1,166 @@ +// Command removedpackages refuses a Go source of the catalogue that imports what mesh-sdk gave up in the release that +// narrowed it (novox/hq ADR 0593 §1): a package that left for its owner, and the build runner's names that left +// mesh-sdk's buildstep for mesh-build-go's package. +// +// A module compiles against the mesh-sdk version it pins, so a source that still names a removed package builds until the +// pin moves, and then breaks in a delivery of its own; and a later change could bring the import back with the old pin. +// This check refuses it at once, from the source alone: +// +// - an import of a package in Removed (or one under it); +// - in a file importing mesh-sdk's buildstep (under any name), a selector naming anything that is not in +// BuildstepContract: what is left of that package is what the delivery parts read, and the runner is +// mesh-build-go's package (git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep). +// +// It reads the non-test and test Go files under modules/ and checks/ (testdata, vendor and node_modules left out). +// +// go run . +// +// Exits 1 on a finding. The lists are the release's: a package that goes later is added here with the change. +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/fs" + "os" + "path/filepath" + "sort" + "strconv" + "strings" +) + +const sdk = "git.novox.be/novox/mesh-sdk/go" + +// Removed are the packages of mesh-sdk that left it, each with where its logic is now. +var Removed = map[string]string{ + "buildcpu": "the build runner's package, git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/buildcpu", + "buildstep/conformance": "the build runner's package, git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/conformance", + "delivery/buildrun": "the builder's package (mesh-delivery-builder), imported by its repository's modules", + "compose": "the node-engine, which alone composes (ADR 0593 §6)", + "compose/frombus": "the node-engine, which alone composes (ADR 0593 §6)", + "compose/fromruntime": "the node-engine, which alone composes (ADR 0593 §6)", + "compose/judge": "the assigner's own package", + "takein": "the node-engine, which alone composes (ADR 0593 §6)", + "validate": "the node-engine, which alone composes (ADR 0593 §6)", + "facts": "nothing: the controller they served is ended (ADR 0590)", + "example": "nothing: no module imports it (ADR 0593 §12)", + "packageguard": "nothing: a test helper of mesh-sdk's own", +} + +// BuildstepContract are the names mesh-sdk's buildstep keeps: the failure contract, the fetch request, the stale-step sweep, +// the step id and the module tree shape the delivery parts read. +var BuildstepContract = map[string]bool{ + "AskWhat": true, "Boot": true, "By": true, "CapacitySetting": true, "Clip": true, "ConsoleFailure": true, "CutLine": true, + "EndOne": true, "EndStale": true, "EndStaleAtStart": true, "EndStaleRound": true, "Failure": true, "FetchRequest": true, + "FetchRequestProperties": true, "Hidden": true, "Launchers": true, "LauncherSuffix": true, "LineBytes": true, + "ModuleTree": true, "NewID": true, "OutputBytes": true, "Owner": true, "PackagesArg": true, "ParseFetchRequest": true, + "PassingLine": true, "RestartReason": true, "Scan": true, "ScanOf": true, "StaleAttempts": true, "StaleRetry": true, + "StaleServed": true, +} + +// Findings is every refusal in the Go sources under root, sorted. +func Findings(root string) ([]string, error) { + var out []string + for _, top := range []string{"modules", "checks"} { + err := filepath.WalkDir(filepath.Join(root, top), func(path string, d fs.DirEntry, err error) error { + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if d.IsDir() { + switch d.Name() { + case "testdata", "vendor", "node_modules", ".mesh-build": + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(path, ".go") { + return nil + } + rel, _ := filepath.Rel(root, path) + found, err := fileFindings(path, filepath.ToSlash(rel)) + if err != nil { + return err + } + out = append(out, found...) + return nil + }) + if err != nil { + return nil, err + } + } + sort.Strings(out) + return out, nil +} + +func fileFindings(path, rel string) ([]string, error) { + file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution) + if err != nil { + return nil, fmt.Errorf("%s: %w", rel, err) + } + var out []string + buildstep := "" + for _, imp := range file.Imports { + p, err := strconv.Unquote(imp.Path.Value) + if err != nil { + continue + } + sub, ok := strings.CutPrefix(p, sdk+"/") + if !ok { + continue + } + for pkg, to := range Removed { + if sub == pkg || strings.HasPrefix(sub, pkg+"/") { + out = append(out, fmt.Sprintf("%s: imports %s, which left mesh-sdk; it is %s (novox/hq ADR 0593 §1)", rel, p, to)) + break + } + } + if sub == "buildstep" { + buildstep = "buildstep" + if imp.Name != nil { + buildstep = imp.Name.Name + } + } + } + if buildstep == "" || buildstep == "_" || buildstep == "." { + return out, nil + } + seen := map[string]bool{} + ast.Inspect(file, func(n ast.Node) bool { + sel, ok := n.(*ast.SelectorExpr) + if !ok { + return true + } + id, ok := sel.X.(*ast.Ident) + if !ok || id.Name != buildstep || !sel.Sel.IsExported() || BuildstepContract[sel.Sel.Name] || seen[sel.Sel.Name] { + return true + } + seen[sel.Sel.Name] = true + out = append(out, fmt.Sprintf("%s: uses %s.%s, which is the build runner's, no longer mesh-sdk's buildstep; import git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep (novox/hq ADR 0593 §1)", rel, buildstep, sel.Sel.Name)) + return true + }) + return out, nil +} + +func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "usage: removedpackages ") + os.Exit(2) + } + found, err := Findings(os.Args[1]) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + for _, f := range found { + fmt.Println(f) + } + if len(found) > 0 { + os.Exit(1) + } + fmt.Println("no source imports a package mesh-sdk gave up") +} diff --git a/checks/removedpackages/main_test.go b/checks/removedpackages/main_test.go new file mode 100644 index 00000000..c5c403c4 --- /dev/null +++ b/checks/removedpackages/main_test.go @@ -0,0 +1,57 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// ADR 0593 §1: a source that imports a package mesh-sdk gave up, or names the build runner through mesh-sdk's buildstep, is +// refused naming where it went; the contract that stays, the runner's own package and a testdata copy are not. +func TestFindingsRefuseWhatMeshSDKGaveUp(t *testing.T) { + const head = "package x\n\nimport (\n" + for _, row := range []struct { + name, file, src string + want string // empty: clean + }{ + {"clean", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/delivery\"\n)\n\nvar _ = delivery.StepsState\n", ""}, + {"a removed package", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/compose\"\n)\n\nvar _ = compose.Resolve\n", "imports git.novox.be/novox/mesh-sdk/go/compose, which left mesh-sdk; it is the node-engine"}, + {"under a removed package", "modules/a/x_test.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/compose/frombus\"\n)\n", "the node-engine"}, + {"buildcpu", "checks/c/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/buildcpu\"\n)\n", "buildstep/buildcpu"}, + {"the runner through mesh-sdk's buildstep", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/buildstep\"\n)\n\nvar _ = buildstep.RunAsk\n", "uses buildstep.RunAsk, which is the build runner's"}, + {"the runner through an alias", "modules/a/x.go", head + "\tsdkstep \"git.novox.be/novox/mesh-sdk/go/buildstep\"\n)\n\nvar _ = sdkstep.Puller{}\n", "uses sdkstep.Puller"}, + {"the contract of buildstep", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/buildstep\"\n)\n\nvar _ = buildstep.NewID\nvar _ buildstep.Failure\n", ""}, + {"the runner's own package", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep\"\n)\n\nvar _ = buildstep.RunAsk\n", ""}, + {"a copy in testdata", "modules/a/testdata/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/compose\"\n)\n", ""}, + } { + t.Run(row.name, func(t *testing.T) { + root := t.TempDir() + p := filepath.Join(root, row.file) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte(row.src), 0o644); err != nil { + t.Fatal(err) + } + got, err := Findings(root) + if err != nil { + t.Fatal(err) + } + if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) { + t.Fatalf("findings = %q, want one containing %q", all, row.want) + } + }) + } +} + +// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2). +func TestTheCatalogueImportsNothingMeshSDKGaveUp(t *testing.T) { + got, err := Findings(filepath.Join("..", "..")) + if err != nil { + t.Fatal(err) + } + for _, f := range got { + t.Error(f) + } +} diff --git a/checks/removedpackages/reads-outside b/checks/removedpackages/reads-outside new file mode 100644 index 00000000..55f733da --- /dev/null +++ b/checks/removedpackages/reads-outside @@ -0,0 +1,6 @@ +# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole; +# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept +# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing. +# every Go source of the modules and of the checks, whose imports are read. +modules +checks diff --git a/checks/sdkjudge/main.go b/checks/sdkjudge/main.go index ac7bc0c9..eea7e734 100644 --- a/checks/sdkjudge/main.go +++ b/checks/sdkjudge/main.go @@ -36,13 +36,7 @@ import ( // composition is every function or table the check lets stay in mesh-sdk, as "path-from-go/:name", and why. It names no // manifest judgement: each is the composer's refusal of composition itself (ADR 0578 clause 5). var composition = map[string]string{ - "catalogue/consumers.go:consumersPlaceholderProblems": "composition: rendering a consumer's template", - "catalogue/collections.go:collectedPlaceholderProblems": "composition: placeholders a contribution renders", - "catalogue/environment_into.go:placeholderProblems": "composition: placeholders a contribution renders", - "catalogue/seat_contributions.go:seatPlaceholderProblems": "composition: placeholders a contribution renders", - "catalogue/collections.go:itemTemplateProblem": "composition: filling an item's template", - "catalogue/setting_takes.go:takenSettingsProblems": "composition: whether stored settings still compose", - "catalogue/template.go:HeldTemplateProblem": "composition: the take-in refusal of a resolved build", + "catalogue/collections.go:itemTemplateProblem": "composition: filling an item's template", } // Problems is what mesh-sdk's Go tree under root (its go/ directory) holds of a manifest judge, one line each, but for diff --git a/modules/adwaita/go.mod b/modules/adwaita/go.mod index eb8b29a5..fadc1746 100644 --- a/modules/adwaita/go.mod +++ b/modules/adwaita/go.mod @@ -2,7 +2,7 @@ module adwaita go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/adwaita/go.sum b/modules/adwaita/go.sum index 655670ff..02d6daae 100644 --- a/modules/adwaita/go.sum +++ b/modules/adwaita/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/artifact-store-tools/go.mod b/modules/artifact-store-tools/go.mod index 599391fa..dfdeecbe 100644 --- a/modules/artifact-store-tools/go.mod +++ b/modules/artifact-store-tools/go.mod @@ -3,7 +3,7 @@ module artifactstoretools go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/artifact-store-tools/go.sum b/modules/artifact-store-tools/go.sum index ebd4e0f2..43cc4ec7 100644 --- a/modules/artifact-store-tools/go.sum +++ b/modules/artifact-store-tools/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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= diff --git a/modules/asus-zephyrus-g14/go.mod b/modules/asus-zephyrus-g14/go.mod index 1affd175..a9d7eaf0 100644 --- a/modules/asus-zephyrus-g14/go.mod +++ b/modules/asus-zephyrus-g14/go.mod @@ -2,7 +2,7 @@ module asuszephyrusg14 go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/asus-zephyrus-g14/go.sum b/modules/asus-zephyrus-g14/go.sum index 655670ff..02d6daae 100644 --- a/modules/asus-zephyrus-g14/go.sum +++ b/modules/asus-zephyrus-g14/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/avahi/go.mod b/modules/avahi/go.mod index de5d61e2..49a0351a 100644 --- a/modules/avahi/go.mod +++ b/modules/avahi/go.mod @@ -2,7 +2,7 @@ module avahi go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/avahi/go.sum b/modules/avahi/go.sum index 655670ff..02d6daae 100644 --- a/modules/avahi/go.sum +++ b/modules/avahi/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/blueman/go.mod b/modules/blueman/go.mod index ac005193..3560b96c 100644 --- a/modules/blueman/go.mod +++ b/modules/blueman/go.mod @@ -2,7 +2,7 @@ module blueman go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/blueman/go.sum b/modules/blueman/go.sum index 655670ff..02d6daae 100644 --- a/modules/blueman/go.sum +++ b/modules/blueman/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/bluetooth/go.mod b/modules/bluetooth/go.mod index 964dfaba..b1929351 100644 --- a/modules/bluetooth/go.mod +++ b/modules/bluetooth/go.mod @@ -2,7 +2,7 @@ module bluetooth go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/bluetooth/go.sum b/modules/bluetooth/go.sum index 655670ff..02d6daae 100644 --- a/modules/bluetooth/go.sum +++ b/modules/bluetooth/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/brother/go.mod b/modules/brother/go.mod index 49887617..5a2a508d 100644 --- a/modules/brother/go.mod +++ b/modules/brother/go.mod @@ -3,7 +3,7 @@ module brother go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/brother/go.sum b/modules/brother/go.sum index ebd4e0f2..43cc4ec7 100644 --- a/modules/brother/go.sum +++ b/modules/brother/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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= diff --git a/modules/claude-code/go.mod b/modules/claude-code/go.mod index 88ab7bdb..0d302dba 100644 --- a/modules/claude-code/go.mod +++ b/modules/claude-code/go.mod @@ -2,7 +2,7 @@ module claude-code go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require gopkg.in/yaml.v3 v3.0.1 diff --git a/modules/claude-code/go.sum b/modules/claude-code/go.sum index a11c9aa4..9f845fe7 100644 --- a/modules/claude-code/go.sum +++ b/modules/claude-code/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/claude-licence-manager/go.mod b/modules/claude-licence-manager/go.mod index fe684b1b..ad85fa04 100644 --- a/modules/claude-licence-manager/go.mod +++ b/modules/claude-licence-manager/go.mod @@ -3,7 +3,7 @@ module claude-licence-manager go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 github.com/jackc/pgx/v5 v5.11.0 ) diff --git a/modules/claude-licence-manager/go.sum b/modules/claude-licence-manager/go.sum index d345545c..7ab29444 100644 --- a/modules/claude-licence-manager/go.sum +++ b/modules/claude-licence-manager/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/modules/clipmenu/go.mod b/modules/clipmenu/go.mod index 61f36353..b412d8ed 100644 --- a/modules/clipmenu/go.mod +++ b/modules/clipmenu/go.mod @@ -2,7 +2,7 @@ module clipmenu go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/clipmenu/go.sum b/modules/clipmenu/go.sum index 655670ff..02d6daae 100644 --- a/modules/clipmenu/go.sum +++ b/modules/clipmenu/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/cups/go.mod b/modules/cups/go.mod index a65da3ad..f060239c 100644 --- a/modules/cups/go.mod +++ b/modules/cups/go.mod @@ -2,7 +2,7 @@ module cups go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/cups/go.sum b/modules/cups/go.sum index 655670ff..02d6daae 100644 --- a/modules/cups/go.sum +++ b/modules/cups/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/dbus/go.mod b/modules/dbus/go.mod index cd9a329c..78f03450 100644 --- a/modules/dbus/go.mod +++ b/modules/dbus/go.mod @@ -3,7 +3,7 @@ module dbus go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 github.com/godbus/dbus/v5 v5.1.0 ) diff --git a/modules/dbus/go.sum b/modules/dbus/go.sum index f62c1e17..0a96ebed 100644 --- a/modules/dbus/go.sum +++ b/modules/dbus/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= diff --git a/modules/desk-channel/go.mod b/modules/desk-channel/go.mod index 88a561f6..3ee5c902 100644 --- a/modules/desk-channel/go.mod +++ b/modules/desk-channel/go.mod @@ -2,7 +2,7 @@ module deskchannel go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/desk-channel/go.sum b/modules/desk-channel/go.sum index 655670ff..02d6daae 100644 --- a/modules/desk-channel/go.sum +++ b/modules/desk-channel/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/disk-load/go.mod b/modules/disk-load/go.mod index dfcf98ae..7d8d3f33 100644 --- a/modules/disk-load/go.mod +++ b/modules/disk-load/go.mod @@ -3,7 +3,7 @@ module diskload go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/disk-load/go.sum b/modules/disk-load/go.sum index ebd4e0f2..43cc4ec7 100644 --- a/modules/disk-load/go.sum +++ b/modules/disk-load/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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= diff --git a/modules/dmenu/go.mod b/modules/dmenu/go.mod index 5237959e..53d7c705 100644 --- a/modules/dmenu/go.mod +++ b/modules/dmenu/go.mod @@ -2,7 +2,7 @@ module dmenu go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/dmenu/go.sum b/modules/dmenu/go.sum index 655670ff..02d6daae 100644 --- a/modules/dmenu/go.sum +++ b/modules/dmenu/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/dnsmasq/go.mod b/modules/dnsmasq/go.mod index eca7351e..320911a4 100644 --- a/modules/dnsmasq/go.mod +++ b/modules/dnsmasq/go.mod @@ -2,7 +2,7 @@ module dnsmasq go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/dnsmasq/go.sum b/modules/dnsmasq/go.sum index 655670ff..02d6daae 100644 --- a/modules/dnsmasq/go.sum +++ b/modules/dnsmasq/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/docker-compose/go.mod b/modules/docker-compose/go.mod index c534f9c3..d80e746b 100644 --- a/modules/docker-compose/go.mod +++ b/modules/docker-compose/go.mod @@ -2,7 +2,7 @@ module docker-compose go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/docker-compose/go.sum b/modules/docker-compose/go.sum index 655670ff..02d6daae 100644 --- a/modules/docker-compose/go.sum +++ b/modules/docker-compose/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/docker/go.mod b/modules/docker/go.mod index 21e968ab..9f395563 100644 --- a/modules/docker/go.mod +++ b/modules/docker/go.mod @@ -2,7 +2,7 @@ module docker go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/docker/go.sum b/modules/docker/go.sum index 655670ff..02d6daae 100644 --- a/modules/docker/go.sum +++ b/modules/docker/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/dunst/go.mod b/modules/dunst/go.mod index ff28c4b9..37fbba41 100644 --- a/modules/dunst/go.mod +++ b/modules/dunst/go.mod @@ -2,7 +2,7 @@ module dunst go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/dunst/go.sum b/modules/dunst/go.sum index 655670ff..02d6daae 100644 --- a/modules/dunst/go.sum +++ b/modules/dunst/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/fail2ban/go.mod b/modules/fail2ban/go.mod index 744f5199..1a247848 100644 --- a/modules/fail2ban/go.mod +++ b/modules/fail2ban/go.mod @@ -2,7 +2,7 @@ module fail2ban go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/fail2ban/go.sum b/modules/fail2ban/go.sum index 655670ff..02d6daae 100644 --- a/modules/fail2ban/go.sum +++ b/modules/fail2ban/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/feh/go.mod b/modules/feh/go.mod index e30b0520..1998c0ca 100644 --- a/modules/feh/go.mod +++ b/modules/feh/go.mod @@ -2,7 +2,7 @@ module feh go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/feh/go.sum b/modules/feh/go.sum index 655670ff..02d6daae 100644 --- a/modules/feh/go.sum +++ b/modules/feh/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/flatpak/go.mod b/modules/flatpak/go.mod index 8f75bbeb..ba689508 100644 --- a/modules/flatpak/go.mod +++ b/modules/flatpak/go.mod @@ -2,7 +2,7 @@ module flatpak go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/flatpak/go.sum b/modules/flatpak/go.sum index 655670ff..02d6daae 100644 --- a/modules/flatpak/go.sum +++ b/modules/flatpak/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/fonts/go.mod b/modules/fonts/go.mod index 37d85c9f..918070bf 100644 --- a/modules/fonts/go.mod +++ b/modules/fonts/go.mod @@ -2,7 +2,7 @@ module fonts go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/fonts/go.sum b/modules/fonts/go.sum index 655670ff..02d6daae 100644 --- a/modules/fonts/go.sum +++ b/modules/fonts/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/forticlient/go.mod b/modules/forticlient/go.mod index 2a04a9e9..4e5ec2da 100644 --- a/modules/forticlient/go.mod +++ b/modules/forticlient/go.mod @@ -2,7 +2,7 @@ module forticlient go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/forticlient/go.sum b/modules/forticlient/go.sum index 655670ff..02d6daae 100644 --- a/modules/forticlient/go.sum +++ b/modules/forticlient/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/gitea/client.ts b/modules/gitea/client.ts index 14ced5b1..9ccf741c 100644 --- a/modules/gitea/client.ts +++ b/modules/gitea/client.ts @@ -559,8 +559,14 @@ export class GiteaClient { * * **A listing the forge cut is no listing** (it says `truncated` on a page with nothing more to read, or the pages * run past a bound): the call fails naming the repository and the commit, never answering the directories it did - * see, since a directory left out would be taken for one without a module. Reads only. */ - async commitTrees(owner: string, repo: string, sha: string, paths?: Record): Promise<{ root: string; trees: Record; paths?: Record> }> { + * see, since a directory left out would be taken for one without a module. With `files`, a list of directories (the + * root as `""`), the answer also carries, of each, every blob under it, recursively, as its path relative to the + * directory with its git object id (the indexer digests the files of a mesh package from it, novox/hq ADR 0593 §2); a + * directory the commit lacks fails naming it. Reads only. */ + async commitTrees(owner: string, repo: string, sha: string, paths?: Record, files?: string[]): Promise<{ root: string; trees: Record; paths?: Record>; files?: Record> }> { + for (const dir of files ?? []) { + if (dir !== "" && dir.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) throw new Error(`files: ${JSON.stringify(dir)} is not a plain relative directory`); + } const base = `${repoPath(owner, repo)}/git/trees/${commitSegment(sha)}?recursive=true`; const cut = (why: string) => new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} was cut (${why}); no trees are answered from a part`); const entries: any[] = []; @@ -587,7 +593,22 @@ export class GiteaClient { if (!tree) throw new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} holds ${e.path} but not the tree of ${dir}`); trees[dir] = tree; } - if (paths === undefined) return { root, trees }; + let filesOf: Record> | undefined; + if (files !== undefined) { + // **The files of whole directories** (novox/hq ADR 0593 §2): every blob under each, from the same listing. The listing + // is whole (a cut one failed above), so a directory absent from it is absent at the commit. + filesOf = {}; + for (const dir of files) { + if (dir !== "" && dirOf.get(dir) === undefined) throw new Error(`the directory ${dir} is not in ${owner}/${repo} at ${sha}`); + const prefix = dir === "" ? "" : `${dir}/`; + const blobs: Record = {}; + for (const e of entries) { + if (e?.type === "blob" && typeof e.path === "string" && e.path.startsWith(prefix)) blobs[e.path.slice(prefix.length)] = String(e.sha); + } + filesOf[dir] = blobs; + } + } + if (paths === undefined) return filesOf === undefined ? { root, trees } : { root, trees, files: filesOf }; // **Named paths** (novox/hq ADR 0499 §3, issue 1758): each asked path's git object id at the commit, a file's or a // directory's, from the same listing, by directory and by path relative to it. A path the listing lacks fails // naming both, and nothing is answered in part. @@ -602,7 +623,7 @@ export class GiteaClient { named[dir][p] = id; } } - return { root, trees, paths: named }; + return filesOf === undefined ? { root, trees, paths: named } : { root, trees, paths: named, files: filesOf }; } /** Whether a repository holds a file at a commit: true, false for a 404, thrown when the forge cannot say. */ diff --git a/modules/gitea/go.mod b/modules/gitea/go.mod index afd6cf2c..56c3ae98 100644 --- a/modules/gitea/go.mod +++ b/modules/gitea/go.mod @@ -3,7 +3,7 @@ module gitea go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/modules/gitea/go.sum b/modules/gitea/go.sum index ebd4e0f2..43cc4ec7 100644 --- a/modules/gitea/go.sum +++ b/modules/gitea/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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= diff --git a/modules/gitea/test/commit-trees.test.ts b/modules/gitea/test/commit-trees.test.ts index 0c36f409..2be41705 100644 --- a/modules/gitea/test/commit-trees.test.ts +++ b/modules/gitea/test/commit-trees.test.ts @@ -221,3 +221,58 @@ test("gitea_commit_trees takes paths as {directory: [paths]} and refuses another server.close(); } }); + +test("files: every blob under each asked directory, recursively, as its path relative to it with the blob's object id (ADR 0593 §2)", async () => { + const { client, server } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]); + try { + const got = await client.commitTrees("novox", "hq", COMMIT, undefined, ["modules/a", "modules/b", ""]); + assert.deepEqual(got.files, { + "modules/a": { "main.go": h("6"), "module.json": h("5") }, + "modules/b": { "c/module.json": h("9") }, + "": { "README.md": h("1"), "module.json": h("2"), "modules/a/module.json": h("5"), "modules/a/main.go": h("6"), "modules/b/c/module.json": h("9"), "modules/d/notes.md": h("b") }, + }); + assert.equal((await client.commitTrees("novox", "hq", COMMIT)).files, undefined, "no files asked, none answered"); + } finally { + server.close(); + } +}); + +test("a directory of files the commit lacks, or spelled badly, or a listing the forge cut, fails and answers nothing", async () => { + const { client, server } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]); + try { + await assert.rejects(client.commitTrees("novox", "hq", COMMIT, undefined, ["modules/a", "modules/gone"]), (err: Error) => { + assert.match(err.message, /modules\/gone/); + return true; + }); + await assert.rejects(client.commitTrees("novox", "hq", COMMIT, undefined, ["README.md"]), /README\.md/, "a file is no directory"); + for (const bad of ["../x", "/abs", "a//b", "modules/a/", "."]) { + await assert.rejects(client.commitTrees("novox", "hq", COMMIT, undefined, [bad]), /files/, bad); + } + } finally { + server.close(); + } + const cut = await serve([{ sha: COMMIT, tree: LISTING, truncated: true, page: 1, total_count: LISTING.length }]); + try { + await assert.rejects(cut.client.commitTrees("novox", "hq", COMMIT, undefined, ["modules/a"]), /cut/); + } finally { + cut.server.close(); + } +}); + +test("gitea_commit_trees takes files as a list of directories and refuses another shape before the forge is asked", async () => { + const { getGiteaTools } = await import("../tools/index.ts"); + const { client, server, asked } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]); + try { + const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!; + assert.ok("files" in tool.input, "the tool names the argument"); + const got: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, files: ["modules/a"] } as any); + assert.deepEqual(got.files, { "modules/a": { "main.go": h("6"), "module.json": h("5") } }); + const before = asked.length; + for (const files of ["modules/a", { "modules/a": [] }, [1], ["../x"]]) { + await assert.rejects(tool.run({ owner: "novox", repo: "hq", sha: COMMIT, files } as any), /files/, JSON.stringify(files)); + } + assert.equal(asked.length, before, "the forge was asked for a refused call"); + } finally { + server.close(); + } +}); diff --git a/modules/gitea/tools/index.ts b/modules/gitea/tools/index.ts index 4d1a94db..9b183a55 100644 --- a/modules/gitea/tools/index.ts +++ b/modules/gitea/tools/index.ts @@ -79,6 +79,17 @@ function namedPaths(raw: unknown): Record | undefined { return out; } +/** The `files` argument of gitea_commit_trees: a list of directories, each plain ("" is the root; no empty, `.` or `..` + * segment, not absolute, no trailing slash); anything else is refused before the forge is asked. Absent is absent. */ +function filesDirs(raw: unknown): string[] | undefined { + if (raw === undefined || raw === null) return undefined; + if (!Array.isArray(raw) || raw.some((d) => typeof d !== "string")) throw new Error("files is a list of directories"); + for (const d of raw as string[]) { + if (d !== "" && d.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) throw new Error(`files: ${JSON.stringify(d)} is not a plain relative directory`); + } + return raw as string[]; +} + export function getGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin, source: SourceRead = sourceFromBus(state)): ToolDefinition[] { return namesChecked(allGiteaTools(gitea, admin, source)); } @@ -722,19 +733,20 @@ function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined }, { name: "gitea_commit_trees", - description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). With paths, {directory: [path relative to it]}, the answer also carries paths, {directory: {path: git object id}} of each named file or directory at the commit (novox/hq ADR 0499 §3); a path the commit lacks fails naming the directory and the path. The commit is a full 40-hex sha. A listing the forge cut is refused, never answered in part. Reads only.", + description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). With files, a list of directories, the answer also carries files, {directory: {path relative to it: git blob id}}, every blob under each directory, recursively, from the same listing (novox/hq ADR 0593 §2); a directory the commit lacks fails naming it. With paths, {directory: [path relative to it]}, the answer also carries paths, {directory: {path: git object id}} of each named file or directory at the commit (novox/hq ADR 0499 §3); a path the commit lacks fails naming the directory and the path. The commit is a full 40-hex sha. A listing the forge cut is refused, never answered in part. Reads only.", input: { owner: { type: "string", description: "the repository owner" }, repo: { type: "string", description: "the repository name" }, sha: { type: "string", description: "the commit, its full 40-hex sha" }, paths: { type: "object", description: "optional, {directory: [path relative to it]}: the git object id of each named file or directory is answered under paths; the root directory is the empty name" }, + files: { type: "array", description: "optional, a list of directories (the root is the empty name): every blob under each, recursively, is answered under files as {directory: {path relative to it: git blob id}}" }, }, run: async (args) => { const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim(); const sha = String(args.sha ?? "").trim(); if (!owner || !repo) throw new Error("name the repository: owner and repo"); if (!/^[0-9a-f]{40}$/i.test(sha)) throw new Error("sha is a commit's full 40-hex sha"); - return gitea.commitTrees(owner, repo, sha, namedPaths(args.paths)); + return gitea.commitTrees(owner, repo, sha, namedPaths(args.paths), filesDirs(args.files)); }, }, { diff --git a/modules/gnome-keyring/go.mod b/modules/gnome-keyring/go.mod index c471cc17..a3f0e714 100644 --- a/modules/gnome-keyring/go.mod +++ b/modules/gnome-keyring/go.mod @@ -2,7 +2,7 @@ module gnomekeyring go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/gnome-keyring/go.sum b/modules/gnome-keyring/go.sum index 655670ff..02d6daae 100644 --- a/modules/gnome-keyring/go.sum +++ b/modules/gnome-keyring/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/hostname/go.mod b/modules/hostname/go.mod index c357f9c3..9ba71698 100644 --- a/modules/hostname/go.mod +++ b/modules/hostname/go.mod @@ -2,7 +2,7 @@ module hostname go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/hostname/go.sum b/modules/hostname/go.sum index 655670ff..02d6daae 100644 --- a/modules/hostname/go.sum +++ b/modules/hostname/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/i3/go.mod b/modules/i3/go.mod index 7d068b38..43c56353 100644 --- a/modules/i3/go.mod +++ b/modules/i3/go.mod @@ -2,7 +2,7 @@ module i3 go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/i3/go.sum b/modules/i3/go.sum index 655670ff..02d6daae 100644 --- a/modules/i3/go.sum +++ b/modules/i3/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/i3status-rust/go.mod b/modules/i3status-rust/go.mod index 0c50f682..c4780203 100644 --- a/modules/i3status-rust/go.mod +++ b/modules/i3status-rust/go.mod @@ -2,7 +2,7 @@ module i3statusrust go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/i3status-rust/go.sum b/modules/i3status-rust/go.sum index 655670ff..02d6daae 100644 --- a/modules/i3status-rust/go.sum +++ b/modules/i3status-rust/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/kernel/go.mod b/modules/kernel/go.mod index 6900b64f..51c1a119 100644 --- a/modules/kernel/go.mod +++ b/modules/kernel/go.mod @@ -2,7 +2,7 @@ module kernel go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/kernel/go.sum b/modules/kernel/go.sum index 655670ff..02d6daae 100644 --- a/modules/kernel/go.sum +++ b/modules/kernel/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/keycloak/go.mod b/modules/keycloak/go.mod index 8e585c99..ed2ba8c4 100644 --- a/modules/keycloak/go.mod +++ b/modules/keycloak/go.mod @@ -2,7 +2,7 @@ module keycloak go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/keycloak/go.sum b/modules/keycloak/go.sum index 655670ff..02d6daae 100644 --- a/modules/keycloak/go.sum +++ b/modules/keycloak/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/lemurs/go.mod b/modules/lemurs/go.mod index 484405f2..ba2b04c5 100644 --- a/modules/lemurs/go.mod +++ b/modules/lemurs/go.mod @@ -2,7 +2,7 @@ module lemurs go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/lemurs/go.sum b/modules/lemurs/go.sum index 655670ff..02d6daae 100644 --- a/modules/lemurs/go.sum +++ b/modules/lemurs/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/localization/go.mod b/modules/localization/go.mod index 348764e6..b89920c2 100644 --- a/modules/localization/go.mod +++ b/modules/localization/go.mod @@ -2,7 +2,7 @@ module localization go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/localization/go.sum b/modules/localization/go.sum index 655670ff..02d6daae 100644 --- a/modules/localization/go.sum +++ b/modules/localization/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/logrotate/go.mod b/modules/logrotate/go.mod index 48144d02..0b750f9c 100644 --- a/modules/logrotate/go.mod +++ b/modules/logrotate/go.mod @@ -2,7 +2,7 @@ module logrotate go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/logrotate/go.sum b/modules/logrotate/go.sum index 655670ff..02d6daae 100644 --- a/modules/logrotate/go.sum +++ b/modules/logrotate/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/memory-pressure/go.mod b/modules/memory-pressure/go.mod index 03102de2..64a31736 100644 --- a/modules/memory-pressure/go.mod +++ b/modules/memory-pressure/go.mod @@ -2,7 +2,7 @@ module memorypressure go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 require ( github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/memory-pressure/go.sum b/modules/memory-pressure/go.sum index 655670ff..02d6daae 100644 --- a/modules/memory-pressure/go.sum +++ b/modules/memory-pressure/go.sum @@ -1,5 +1,5 @@ -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= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/mesh-build-go/README.md b/modules/mesh-build-go/README.md index 51619357..ff16837f 100644 --- a/modules/mesh-build-go/README.md +++ b/modules/mesh-build-go/README.md @@ -133,10 +133,10 @@ agent's recipe (mesh-controller's `toolchain.go`, the Go entry, copied in `testd `builder.go`): `CGO_ENABLED=0`, `GOFLAGS=-trimpath -buildvcs=false` (with `-mod=mod`, or `-mod=vendor` for a directory that vendors), `go build -ldflags "-s -w -X main.builtFor=" -o .mesh-build// ./` in the module's directory, by the installed go (`GOTOOLCHAIN=local`) from the module cache the fetch filled and no network -(`GOPROXY=off`). The binary is named as the build agent names it. The output directory is packed with mesh-sdk's `Pack`, +(`GOPROXY=off`). The binary is named as the build agent names it. The output directory is packed with the build runner's `Pack`, as the build agent packs; two build nodes on the same toolchain give the same digest for the same tree (ADR 0477 §6). The step ends with the artifact, the binary, the digest, the size and the reference of the archive: once the build ended, this process pushes it to the artifact store (novox/hq ADR 0498 §1 §2, -mesh-sdk's `buildstep.PushBlob`) under `/` by its digest, a candidate's as a trunk build's, and answers +the build runner's `buildstep.PushBlob`) under `/` by its digest, a candidate's as a trunk build's, and answers `artifact-store:////blobs/`; a push that fails fails the step `not stored: : `. The packed bytes never stay on the node. The store's address is `${dir:state}/registry`, placed by the manifest, which requires `artifact-store`. The go build runs shut off in bubblewrap (ADR 0448, `isolate.go`): no @@ -148,3 +148,20 @@ directory named by `from`, or a file pinned by `url` and `sha256`, packed with t (`testdata/captured/README` says how its digest was captured). A pinned file is fetched, checked against its sha256 (a mismatch fails the step naming both digests) and laid out as the artifact says (`file`, or unpacked from `within`; gzip, bzip2 and xz, read in Go as the build agent reads it, issue 1590); it runs no command and needs no Go. + +## The runner's package, and what a build says at its end (ADR 0593) + +- The build runner (the pull of asks, the step lifecycle, the toolchain guard, the fetch, the parsers of `go test -json` and + `node --test`, `Pack`, the conformance kit and the `mesh-pack` command) is this module's package + `git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep`, a Go module of its own in `buildstep/`. mesh-build-node + and mesh-build-image require it and replace it to the sibling directory, since it is the same repository. What the delivery + parts read of a step (the failure contract, the fetch request, the stale-step sweep, the step id) stays in mesh-sdk's + `buildstep`. +- `build_go_imports` (a tool, and the last step of a build ask that built an artifact) answers the mesh packages the module's + Go programs import: `go list -deps` of each Go bundle's program, tests left out, the packages of the forge outside the + module's own directory (a sibling package of its own repository counts), sorted and each once. A build ask carries them as + `BuildAnswer.Imports`; the indexer takes the module's fingerprint over those packages' content. +- A merge preview (`build_go_fetch` with a merge) answers raw trees: each module directory's git tree, its manifest and lock + digests and the objects of the paths its artifacts name. The indexer computes the named tree. +- A package of the mesh's forge counts in a test digest by the content of the files the tests reach, not by its version, so a + merge of a package whose reached files are unchanged keeps every kept verdict. diff --git a/modules/mesh-build-go/buildstep/askrun.go b/modules/mesh-build-go/buildstep/askrun.go new file mode 100644 index 00000000..60bc385e --- /dev/null +++ b/modules/mesh-build-go/buildstep/askrun.go @@ -0,0 +1,497 @@ +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 + // Imports reads the mesh packages the built programs import (the Go module's alone, ADR 0593 §2): it answers + // {"imports": [...]}, sorted, from the workspace the build ran in. A build that built an artifact asks it once, at + // its end; nil reports none. + Imports 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}) + } + if s.Imports != nil && f.Workspace != "" { + out, err := s.Imports(ctx, map[string]any{"workspace": f.Workspace, "module": job.Path}) + if err != nil { + return nil, fmt.Errorf("the imports of %s: %w", job.Module, err) + } + r, err := raw(out) + if err != nil { + return nil, err + } + var read struct { + Imports []string `json:"imports"` + } + if err := json.Unmarshal(r, &read); err != nil { + return nil, fmt.Errorf("the imports of %s answered what is not a list: %s", job.Module, r) + } + if err := delivery.CheckImports(read.Imports); err != nil { + return nil, err + } + answer.Imports = read.Imports + } + 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..b0fce397 --- /dev/null +++ b/modules/mesh-build-go/buildstep/askrun_test.go @@ -0,0 +1,336 @@ +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 } + // ADR 0593 §2: a Go module's build ends by saying which mesh packages its programs import. + steps.Imports = func(ctx context.Context, args map[string]any) (any, error) { + b, _ := json.Marshal(args) + r.calls = append(r.calls, "imports "+string(b)) + return map[string]any{"imports": []string{"git.novox.be/novox/mesh-sdk/go/announce", "git.novox.be/novox/mesh-sdk/go/delivery"}}, 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"} +imports {"module":"modules/m","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) + } + if !slices.Equal(answer.Imports, []string{"git.novox.be/novox/mesh-sdk/go/announce", "git.novox.be/novox/mesh-sdk/go/delivery"}) { + t.Errorf("the answer's imports are %v", answer.Imports) + } +} + +// 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_agree_test.go b/modules/mesh-build-go/buildstep/buildcpu/buildcpu_agree_test.go new file mode 100644 index 00000000..bf61988d --- /dev/null +++ b/modules/mesh-build-go/buildstep/buildcpu/buildcpu_agree_test.go @@ -0,0 +1,14 @@ +package buildcpu + +import ( + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// The share's names the composer gives are the build modules' (catalogue writes them too, so it does not import this). +func TestTheShareNamesAgreeWithTheCatalogues(t *testing.T) { + if ShareSetting != catalogue.BuildCPUShareSetting || ShareWord != catalogue.BuildCPUShareWord || DefaultPercent != catalogue.BuildCPUDefaultPercent { + t.Fatal("buildcpu's share names differ from catalogue's") + } +} 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..d61305e9 --- /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.20261102191058-f02f142af128 + 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..43cc4ec7 --- /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.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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