Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4548b97910 | ||
|
|
358ef45536 | ||
|
|
175b2cd791 |
@@ -35,8 +35,6 @@ modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
|
|||||||
/checks/paths/paths
|
/checks/paths/paths
|
||||||
/modules/mesh-build-go/mesh-build-go
|
/modules/mesh-build-go/mesh-build-go
|
||||||
/modules/mesh-delivery-gate/mesh-delivery-gate
|
/modules/mesh-delivery-gate/mesh-delivery-gate
|
||||||
modules/mesh-config/cmd/mesh-config/mesh-config
|
|
||||||
checks/placement/placement
|
|
||||||
|
|
||||||
# Built by their modules' build step (issue 1577), never committed.
|
# Built by their modules' build step (issue 1577), never committed.
|
||||||
modules/forticlient/cmd/forticlient-tools/forticlient-tools
|
modules/forticlient/cmd/forticlient-tools/forticlient-tools
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest and the files it names.
|
|
||||||
modules
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// Package failurekit refuses a build module whose tests do not run mesh-sdk'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
|
||||||
|
// exempt, and said so.
|
||||||
|
package failurekit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Judged is what the check says of one build module: Refused says why it fails the check, "" when it passes; Exempt
|
||||||
|
// says why the kit does not apply.
|
||||||
|
type Judged struct {
|
||||||
|
Module, Refused, Exempt string
|
||||||
|
}
|
||||||
|
|
||||||
|
type manifest struct {
|
||||||
|
Claims []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"claims"`
|
||||||
|
Tools []json.RawMessage `json:"tools"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Judge reads every modules/<m>/module.json of the catalogue at root and judges each module that claims a node-build-*
|
||||||
|
// seat.
|
||||||
|
func Judge(root string) ([]Judged, error) {
|
||||||
|
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sort.Strings(manifests)
|
||||||
|
var out []Judged
|
||||||
|
for _, p := range manifests {
|
||||||
|
raw, err := os.ReadFile(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var m manifest
|
||||||
|
if err := json.Unmarshal(raw, &m); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", p, err)
|
||||||
|
}
|
||||||
|
seat := ""
|
||||||
|
for _, c := range m.Claims {
|
||||||
|
if strings.HasPrefix(c.Name, "node-build-") {
|
||||||
|
seat = c.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seat == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(p)
|
||||||
|
j := Judged{Module: filepath.Base(dir)}
|
||||||
|
test := testTool(m.Tools)
|
||||||
|
switch {
|
||||||
|
case test == "":
|
||||||
|
j.Exempt = fmt.Sprintf("it claims %s and serves no test step", seat)
|
||||||
|
default:
|
||||||
|
runs, err := runsTheKit(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !runs {
|
||||||
|
j.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, j)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// testTool is the name of the module's test step tool (<prefix>_test), or "".
|
||||||
|
func testTool(tools []json.RawMessage) string {
|
||||||
|
for _, raw := range tools {
|
||||||
|
var name string
|
||||||
|
if json.Unmarshal(raw, &name) != nil {
|
||||||
|
var t struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(raw, &t) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name = t.Name
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(name, "_test") {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// runsTheKit says a _test.go file under dir calls conformance.Run.
|
||||||
|
func runsTheKit(dir string) (bool, error) {
|
||||||
|
found := false
|
||||||
|
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || found || d.IsDir() || !strings.HasSuffix(p, "_test.go") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
found = strings.Contains(string(raw), "conformance.Run(") &&
|
||||||
|
strings.Contains(string(raw), `"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"`)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package failurekit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1901/AC7: a build module with a test step and no kit is refused, one that runs the kit passes, one with no test step is
|
||||||
|
// exempt, and a module that claims no node-build-* seat is not judged.
|
||||||
|
func TestABuildModuleWithATestStepMustRunTheKit(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
kit := "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/buildstep/conformance\"\n\nfunc TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n"
|
||||||
|
for path, body := range map[string]string{
|
||||||
|
"modules/a/module.json": `{"claims":[{"name":"node-build-a"}],"tools":["build_a_fetch","build_a_test"]}`,
|
||||||
|
"modules/a/cmd/a/check_test.go": "package main\n",
|
||||||
|
"modules/b/module.json": `{"claims":[{"name":"node-build-b"}],"tools":[{"name":"build_b_test"}]}`,
|
||||||
|
"modules/b/cmd/b/kit_test.go": kit,
|
||||||
|
"modules/c/module.json": `{"claims":[{"name":"node-build-c"}],"tools":["build_c_build"]}`,
|
||||||
|
"modules/d/module.json": `{"claims":[{"name":"other"}],"tools":["d_test"]}`,
|
||||||
|
"modules/e/module.json": `{"claims":[{"name":"node-build-e"}],"tools":["build_e_test"]}`,
|
||||||
|
"modules/e/cmd/e/kit.go": kit, // not a test file
|
||||||
|
} {
|
||||||
|
p := filepath.Join(root, path)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
got, err := Judge(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := map[string]string{"a": "refused", "b": "passes", "c": "exempt", "e": "refused"}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("judged %+v, want a, b, c and e", got)
|
||||||
|
}
|
||||||
|
for _, j := range got {
|
||||||
|
is := "passes"
|
||||||
|
if j.Refused != "" {
|
||||||
|
is = "refused"
|
||||||
|
} else if j.Exempt != "" {
|
||||||
|
is = "exempt"
|
||||||
|
}
|
||||||
|
if is != want[j.Module] {
|
||||||
|
t.Errorf("%s %s (%+v), want it %s", j.Module, is, j, want[j.Module])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every build module of this catalogue with a test step runs the kit.
|
||||||
|
func TestTheCataloguesBuildModulesRunTheKit(t *testing.T) {
|
||||||
|
got, err := Judge(filepath.Join("..", ".."))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(got) == 0 {
|
||||||
|
t.Fatal("no module claims a node-build-* seat: the check reads the wrong tree")
|
||||||
|
}
|
||||||
|
for _, j := range got {
|
||||||
|
switch {
|
||||||
|
case j.Refused != "":
|
||||||
|
t.Errorf("%s: %s", j.Module, j.Refused)
|
||||||
|
case j.Exempt != "":
|
||||||
|
t.Logf("%s is exempt: %s", j.Module, j.Exempt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module git.novox.be/novox/mesh-catalog/checks/failurekit
|
||||||
|
|
||||||
|
go 1.22
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
@@ -1,10 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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 manifest through mesh-sdk's ReadTreeManifest: the root's predefined keys and each templated module's
|
|
||||||
# templates (files/ by default, or the directory its manifest names); and the root's kept count of resources that do not say how they are ready.
|
|
||||||
modules/*/module.json
|
|
||||||
predefined.json
|
|
||||||
modules/*/files
|
|
||||||
modules/*/templates
|
|
||||||
health-undeclared
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest, and whether the path each image artifact builds from is there.
|
|
||||||
modules
|
|
||||||
|
|||||||
@@ -1,7 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest, a TypeScript module's package.json and whether its lock is there.
|
|
||||||
modules/*/module.json
|
|
||||||
modules/*/package.json
|
|
||||||
modules/*/package-lock.json
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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 file under modules/ (git ls-files and a walk).
|
|
||||||
modules
|
|
||||||
|
|||||||
@@ -1,7 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# that none of the old check scripts stands at the root.
|
|
||||||
?merge-check.sh
|
|
||||||
?merge-check-typescript.sh
|
|
||||||
?check-nested-modules.sh
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest; its lists are this package's own files.
|
|
||||||
modules/*/module.json
|
|
||||||
|
|||||||
@@ -1,6 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# check("../.."): the root's list and every file of the modules it names.
|
|
||||||
particulars
|
|
||||||
modules
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest and every file of a module.
|
|
||||||
modules
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module checks/placement
|
|
||||||
|
|
||||||
go 1.26.0
|
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026210326-979644420117
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026210326-979644420117 h1:88CNZOoQgu7YPYKW16qkWT8Gw9Ucg07aTrIImA2nYUk=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026210326-979644420117/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=
|
|
||||||
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.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
|
||||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// Command placement is the catalogue's check of its placement files (novox/hq ADR 0549 §1, ADR 0555; issue 1126): every
|
|
||||||
// `placement/<node>.json` reads as a placement of the node its name says, and the files together are refused, every
|
|
||||||
// finding named at once, for two modules of one node on one host port, a pin, binding or holding naming a provider not
|
|
||||||
// placed where it says, a seat held once for the mesh held twice, two nodes with one address on the private network, or
|
|
||||||
// more than one hub (mesh-sdk delivery.CheckPlacements).
|
|
||||||
//
|
|
||||||
// A module the catalogue does not hold and a node not enrolled are the composition check's to refuse: placement names
|
|
||||||
// modules of every repository the indexer reads and the nodes the mesh enrolled, neither of which this tree holds.
|
|
||||||
//
|
|
||||||
// go run . <catalogue root>
|
|
||||||
//
|
|
||||||
// Exits 1 on a finding.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check reads every placement file under root and answers each finding; none is a catalogue whose placement holds. A
|
|
||||||
// catalogue with no placement directory has none to check.
|
|
||||||
func Check(root string) ([]string, error) {
|
|
||||||
dir := filepath.Join(root, delivery.PlacementDir)
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var found []string
|
|
||||||
placements := map[string]delivery.Placement{}
|
|
||||||
for _, e := range entries {
|
|
||||||
rel := delivery.PlacementDir + "/" + e.Name()
|
|
||||||
node, ok := delivery.PlacementFileNode(rel)
|
|
||||||
if e.IsDir() || !ok {
|
|
||||||
found = append(found, fmt.Sprintf("%s is not a placement file: the directory holds only <node>.json, one per node", rel))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
raw, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p, err := delivery.DecodePlacement(raw)
|
|
||||||
if err != nil {
|
|
||||||
found = append(found, fmt.Sprintf("%s does not read as a placement: %v", rel, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if p.Revision != "" {
|
|
||||||
found = append(found, fmt.Sprintf("%s names a revision, which only the indexer writes into the key", rel))
|
|
||||||
}
|
|
||||||
placements[node] = p
|
|
||||||
}
|
|
||||||
all := func(string) bool { return true }
|
|
||||||
found = append(found, delivery.CheckPlacements(placements, all, all)...)
|
|
||||||
sort.Strings(found)
|
|
||||||
return found, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) != 2 {
|
|
||||||
fmt.Fprintln(os.Stderr, "usage: placement <catalogue root>")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
found, err := Check(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The catalogue's placement files as the merge check reads them (ADR 0549 §1, ADR 0555): the real tree holds every
|
|
||||||
// rule, and a tree that breaks them is refused with every finding named at once.
|
|
||||||
func TestPlacementFiles(t *testing.T) {
|
|
||||||
found, err := Check("../..")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
t.Errorf("%s (novox/hq ADR 0549 §1)", f)
|
|
||||||
}
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
write := func(name, body string) {
|
|
||||||
t.Helper()
|
|
||||||
if err := os.MkdirAll(filepath.Join(root, "placement"), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(root, "placement", name), []byte(body), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found, err := Check(root); err != nil || len(found) != 0 {
|
|
||||||
t.Fatalf("a catalogue with no placement directory: %v, %v", found, err)
|
|
||||||
}
|
|
||||||
write("ace.json", `{"v":1,"node":"ace","decided":{"hub":true,"address":"10.77.0.1"},"modules":[{"module":"a","ports":{"80":8080}},{"module":"b","ports":{"81":8080}}]}`)
|
|
||||||
write("g14.json", `{"v":1,"node":"g14","decided":{"hub":true,"address":"10.77.0.1"},"modules":[{"module":"c","bindings":{"pg":{"node":"ace","module":"pg"}}}]}`)
|
|
||||||
write("novox.json", `{"v":1,"node":"shanks","modules":[]}`)
|
|
||||||
write("bad.json", `{"v":1,"node":"bad","modules":{}}`)
|
|
||||||
write("dated.json", `{"v":1,"node":"dated","revision":"abc","modules":[]}`)
|
|
||||||
write("notes.md", "x")
|
|
||||||
found, err = Check(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, want := range []string{"a and b are both published on host port 8080", "bound to ace/pg", "both have the address 10.77.0.1",
|
|
||||||
"ace, g14 are each placed as it", "placement file of novox names the node", "bad.json does not read", "dated.json names a revision",
|
|
||||||
"notes.md is not a placement file"} {
|
|
||||||
hit := false
|
|
||||||
for _, f := range found {
|
|
||||||
hit = hit || strings.Contains(f, want)
|
|
||||||
}
|
|
||||||
if !hit {
|
|
||||||
t.Errorf("no finding says %q; found %v", want, found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# the root's list of predefined keys and every manifest that names one.
|
|
||||||
predefined.json
|
|
||||||
modules/*/module.json
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest and the files it names.
|
|
||||||
modules
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest and its templates.
|
|
||||||
modules
|
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
@@ -1,7 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# run("../.."): the root's lists of retired words and lines, and every module's texts.
|
|
||||||
retired-words
|
|
||||||
retired-lines
|
|
||||||
modules
|
|
||||||
|
|||||||
@@ -1,9 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# the copies of this package in every module that carries one.
|
|
||||||
modules/xorg/internal/desktop/*.go
|
|
||||||
modules/lemurs/internal/desktop/*.go
|
|
||||||
modules/i3/internal/desktop/*.go
|
|
||||||
modules/xterm/internal/desktop/*.go
|
|
||||||
modules/adwaita/internal/desktop/*.go
|
|
||||||
|
|||||||
@@ -1,25 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# the copies of desktop.go and its tests in every module that carries one.
|
|
||||||
modules/blueman/cmd/blueman-tools/desktop.go
|
|
||||||
modules/blueman/cmd/blueman-tools/desktop_test.go
|
|
||||||
modules/blueman/cmd/blueman-tools/copies_test.go
|
|
||||||
modules/forticlient/cmd/forticlient-tools/desktop.go
|
|
||||||
modules/forticlient/cmd/forticlient-tools/desktop_test.go
|
|
||||||
modules/forticlient/cmd/forticlient-tools/copies_test.go
|
|
||||||
modules/nextcloud-client/cmd/nextcloud-client-tools/desktop.go
|
|
||||||
modules/nextcloud-client/cmd/nextcloud-client-tools/desktop_test.go
|
|
||||||
modules/nextcloud-client/cmd/nextcloud-client-tools/copies_test.go
|
|
||||||
modules/nm-applet/cmd/nm-applet-tools/desktop.go
|
|
||||||
modules/nm-applet/cmd/nm-applet-tools/desktop_test.go
|
|
||||||
modules/nm-applet/cmd/nm-applet-tools/copies_test.go
|
|
||||||
modules/openrazer/cmd/openrazer-tools/desktop.go
|
|
||||||
modules/openrazer/cmd/openrazer-tools/desktop_test.go
|
|
||||||
modules/openrazer/cmd/openrazer-tools/copies_test.go
|
|
||||||
modules/polychromatic/cmd/polychromatic-tools/desktop.go
|
|
||||||
modules/polychromatic/cmd/polychromatic-tools/desktop_test.go
|
|
||||||
modules/polychromatic/cmd/polychromatic-tools/copies_test.go
|
|
||||||
modules/slack/cmd/slack-tools/desktop.go
|
|
||||||
modules/slack/cmd/slack-tools/desktop_test.go
|
|
||||||
modules/slack/cmd/slack-tools/copies_test.go
|
|
||||||
|
|||||||
@@ -1,6 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# mesh-mail's store.json is the same as brother's; no module scan is left.
|
|
||||||
modules/mesh-mail/files/store.json
|
|
||||||
?modules/scan
|
|
||||||
|
|||||||
@@ -1,25 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# the copies of desktop.go and its tests in every module that carries one.
|
|
||||||
modules/blueman/cmd/blueman-tools/desktop.go
|
|
||||||
modules/blueman/cmd/blueman-tools/desktop_test.go
|
|
||||||
modules/blueman/cmd/blueman-tools/copies_test.go
|
|
||||||
modules/forticlient/cmd/forticlient-tools/desktop.go
|
|
||||||
modules/forticlient/cmd/forticlient-tools/desktop_test.go
|
|
||||||
modules/forticlient/cmd/forticlient-tools/copies_test.go
|
|
||||||
modules/nextcloud-client/cmd/nextcloud-client-tools/desktop.go
|
|
||||||
modules/nextcloud-client/cmd/nextcloud-client-tools/desktop_test.go
|
|
||||||
modules/nextcloud-client/cmd/nextcloud-client-tools/copies_test.go
|
|
||||||
modules/nm-applet/cmd/nm-applet-tools/desktop.go
|
|
||||||
modules/nm-applet/cmd/nm-applet-tools/desktop_test.go
|
|
||||||
modules/nm-applet/cmd/nm-applet-tools/copies_test.go
|
|
||||||
modules/openrazer/cmd/openrazer-tools/desktop.go
|
|
||||||
modules/openrazer/cmd/openrazer-tools/desktop_test.go
|
|
||||||
modules/openrazer/cmd/openrazer-tools/copies_test.go
|
|
||||||
modules/polychromatic/cmd/polychromatic-tools/desktop.go
|
|
||||||
modules/polychromatic/cmd/polychromatic-tools/desktop_test.go
|
|
||||||
modules/polychromatic/cmd/polychromatic-tools/copies_test.go
|
|
||||||
modules/slack/cmd/slack-tools/desktop.go
|
|
||||||
modules/slack/cmd/slack-tools/desktop_test.go
|
|
||||||
modules/slack/cmd/slack-tools/copies_test.go
|
|
||||||
|
|||||||
+12
-102
@@ -174,27 +174,8 @@ function ignoreSecretSetting(cfg: Record<string, unknown>, key: string): void {
|
|||||||
console.error(`the setting ${key} is ignored: it comes from the vault`);
|
console.error(`the setting ${key} is ignored: it comes from the vault`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** How long the forge may take to answer one request before it is cut and said (novox/hq issue 1577: a request for a 5 MB
|
|
||||||
* file never ended, and every read through the module timed out behind it). */
|
|
||||||
export const FORGE_REQUEST_TIMEOUT_MS = 20_000;
|
|
||||||
|
|
||||||
/** The most a tool's answer carries of one file: the bus's message bound is 1 MiB and a file travels as text (issue 1577). */
|
|
||||||
export const MAX_FILE_BYTES = 512 * 1024;
|
|
||||||
|
|
||||||
/** One request to the forge, cut at the limit and failing loud naming the request. */
|
|
||||||
async function timedFetch(url: string, init: RequestInit, label: string, timeoutMs: number): Promise<Response> {
|
|
||||||
try {
|
|
||||||
return await fetch(url, { ...init, signal: init.signal ? AbortSignal.any([init.signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs) });
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error && err.name === "TimeoutError") throw new Error(`Gitea API ${label}: no answer in ${timeoutMs / 1000} s, the request was cut`);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GiteaClient {
|
export class GiteaClient {
|
||||||
readonly baseUrl: string;
|
readonly baseUrl: string;
|
||||||
/** The limit of one request; a test sets it short. */
|
|
||||||
requestTimeoutMs = FORGE_REQUEST_TIMEOUT_MS;
|
|
||||||
private readonly tokens: TokenSource;
|
private readonly tokens: TokenSource;
|
||||||
|
|
||||||
/** A token given as a string is one somebody configured; a source decides for itself (token.ts). */
|
/** A token given as a string is one somebody configured; a source decides for itself (token.ts). */
|
||||||
@@ -249,14 +230,14 @@ export class GiteaClient {
|
|||||||
|
|
||||||
private send(path: string, options: RequestInit, token: string): Promise<Response> {
|
private send(path: string, options: RequestInit, token: string): Promise<Response> {
|
||||||
checkSentPath(path);
|
checkSentPath(path);
|
||||||
return timedFetch(`${this.baseUrl}/api/v1${path}`, {
|
return fetch(`${this.baseUrl}/api/v1${path}`, {
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `token ${token}`,
|
Authorization: `token ${token}`,
|
||||||
...(options.headers as Record<string, string> | undefined),
|
...(options.headers as Record<string, string> | undefined),
|
||||||
},
|
},
|
||||||
}, `${options.method ?? "GET"} ${path}`, this.requestTimeoutMs);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generic authenticated API call — the escape hatch for endpoints without a dedicated method.
|
/** Generic authenticated API call — the escape hatch for endpoints without a dedicated method.
|
||||||
@@ -559,7 +540,7 @@ 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
|
* **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
|
* 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. */
|
* 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<string, string[]>, listed?: string[]): Promise<{ root: string; trees: Record<string, string>; paths?: Record<string, Record<string, string>>; listed?: Record<string, Record<string, string>> }> {
|
async commitTrees(owner: string, repo: string, sha: string, paths?: Record<string, string[]>): Promise<{ root: string; trees: Record<string, string>; paths?: Record<string, Record<string, string>> }> {
|
||||||
const base = `${repoPath(owner, repo)}/git/trees/${commitSegment(sha)}?recursive=true`;
|
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 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[] = [];
|
const entries: any[] = [];
|
||||||
@@ -586,23 +567,7 @@ 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}`);
|
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;
|
trees[dir] = tree;
|
||||||
}
|
}
|
||||||
// **Listed directories** (novox/hq ADR 0549 §2, issue 1126): each asked directory's files directly under it, by name
|
if (paths === undefined) return { root, trees };
|
||||||
// to blob id, from the same listing: how the forge bridge finds the catalogue's placement files, one per node, at a
|
|
||||||
// merge. A directory the commit lacks lists nothing (the catalogue before its first placement file).
|
|
||||||
let files: Record<string, Record<string, string>> | undefined;
|
|
||||||
if (listed !== undefined) {
|
|
||||||
files = {};
|
|
||||||
for (const dir of listed) {
|
|
||||||
files[dir] = {};
|
|
||||||
const prefix = dir === "" ? "" : `${dir}/`;
|
|
||||||
for (const e of entries) {
|
|
||||||
if (e?.type !== "blob" || typeof e.path !== "string" || !e.path.startsWith(prefix)) continue;
|
|
||||||
const name = e.path.slice(prefix.length);
|
|
||||||
if (name !== "" && !name.includes("/")) files[dir][name] = String(e.sha);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (paths === undefined) return files === undefined ? { root, trees } : { root, trees, listed: files };
|
|
||||||
// **Named paths** (novox/hq ADR 0499 §3, issue 1758): each asked path's git object id at the commit, a file's or a
|
// **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
|
// 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.
|
// naming both, and nothing is answered in part.
|
||||||
@@ -617,7 +582,7 @@ export class GiteaClient {
|
|||||||
named[dir][p] = id;
|
named[dir][p] = id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return files === undefined ? { root, trees, paths: named } : { root, trees, paths: named, listed: files };
|
return { root, trees, paths: named };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether a repository holds a file at a commit: true, false for a 404, thrown when the forge cannot say. */
|
/** Whether a repository holds a file at a commit: true, false for a 404, thrown when the forge cannot say. */
|
||||||
@@ -806,22 +771,6 @@ export class GiteaClient {
|
|||||||
/** One file's contents at a ref (default the repository's default branch), decoded. */
|
/** One file's contents at a ref (default the repository's default branch), decoded. */
|
||||||
async getFile(owner: string, repo: string, path: string, ref?: string): Promise<{ path: string; ref?: string; sha: string; size: number; content: string }> {
|
async getFile(owner: string, repo: string, path: string, ref?: string): Promise<{ path: string; ref?: string; sha: string; size: number; content: string }> {
|
||||||
const qs = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
const qs = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
||||||
// The size first, from the file's directory listing, which carries no content: a file over what an answer can carry is
|
|
||||||
// refused without its body being fetched (issue 1577). A file the listing does not show is asked for as before.
|
|
||||||
const clean = path.replace(/^\/+|\/+$/g, "");
|
|
||||||
pathSegments("file path", clean); // a path that is another path or user is refused before anything reaches the forge
|
|
||||||
const slash = clean.lastIndexOf("/");
|
|
||||||
const dir = slash < 0 ? "" : clean.slice(0, slash);
|
|
||||||
let listing: any[] | null = null;
|
|
||||||
try {
|
|
||||||
listing = await this.request<any[]>(`${repoPath(owner, repo)}/contents${dir ? "/" + pathSegments("file path", dir) : ""}${qs}`);
|
|
||||||
} catch (err) {
|
|
||||||
if (!(err instanceof Error) || !/: 404 /.test(err.message)) throw err;
|
|
||||||
}
|
|
||||||
const entry = Array.isArray(listing) ? listing.find((e) => e?.path === clean) : undefined;
|
|
||||||
if (entry && Number(entry.size) > MAX_FILE_BYTES) {
|
|
||||||
throw new Error(`the file is ${Number(entry.size)} bytes, over the ${MAX_FILE_BYTES / 1024} KiB a tool answer can carry; read it from a clone`);
|
|
||||||
}
|
|
||||||
const f = await this.request<any>(`${repoPath(owner, repo)}/contents/${pathSegments("file path", path.replace(/^\/+|\/+$/g, ""))}${qs}`);
|
const f = await this.request<any>(`${repoPath(owner, repo)}/contents/${pathSegments("file path", path.replace(/^\/+|\/+$/g, ""))}${qs}`);
|
||||||
if (!f || f.type !== "file") throw new Error(`Gitea API: ${path} is not a file`);
|
if (!f || f.type !== "file") throw new Error(`Gitea API: ${path} is not a file`);
|
||||||
const content = f.encoding === "base64" ? Buffer.from(String(f.content ?? ""), "base64").toString("utf8") : String(f.content ?? "");
|
const content = f.encoding === "base64" ? Buffer.from(String(f.content ?? ""), "base64").toString("utf8") : String(f.content ?? "");
|
||||||
@@ -991,14 +940,14 @@ export class GiteaAdmin {
|
|||||||
* throwing on it — the caller decides which non-2xx codes are idempotent successes. Only an
|
* throwing on it — the caller decides which non-2xx codes are idempotent successes. Only an
|
||||||
* unexpected status becomes an error, and only where the caller says so. */
|
* unexpected status becomes an error, and only where the caller says so. */
|
||||||
private async request(path: string, options: RequestInit = {}): Promise<AdminResponse> {
|
private async request(path: string, options: RequestInit = {}): Promise<AdminResponse> {
|
||||||
const res = await timedFetch(`${this.baseUrl}/api/v1${path}`, {
|
const res = await fetch(`${this.baseUrl}/api/v1${path}`, {
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: this.authorization,
|
Authorization: this.authorization,
|
||||||
...(options.headers as Record<string, string> | undefined),
|
...(options.headers as Record<string, string> | undefined),
|
||||||
},
|
},
|
||||||
}, `${options.method ?? "GET"} ${path}`, FORGE_REQUEST_TIMEOUT_MS);
|
});
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let body: any = null;
|
let body: any = null;
|
||||||
if (text) {
|
if (text) {
|
||||||
@@ -1098,19 +1047,12 @@ export class GiteaAdmin {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What the forge gives an account on a repository, effective: none, read, write, admin or owner. It is the repository as
|
/** What the forge itself says an account may do on a repository: none, read, write, admin or owner. */
|
||||||
* that account sees it (the site administrator acts as the account with `sudo`), so access through an organisation's team
|
|
||||||
* counts. The collaborator endpoint is not used: it names only a collaborator's own permission, and says "read" of an
|
|
||||||
* account that writes through a team (novox/hq issue 1945). */
|
|
||||||
async repositoryPermission(owner: string, repo: string, user: string): Promise<string> {
|
async repositoryPermission(owner: string, repo: string, user: string): Promise<string> {
|
||||||
const path = `${repoPath(owner, repo)}?sudo=${encodeURIComponent(user)}`;
|
const path = `${repoPath(owner, repo)}/collaborators/${encodeURIComponent(user)}/permission`;
|
||||||
const res = await this.request(path);
|
const res = await this.request(path);
|
||||||
if (res.status === 404) return "none";
|
|
||||||
if (res.status !== 200) GiteaAdmin.fail(path, res);
|
if (res.status !== 200) GiteaAdmin.fail(path, res);
|
||||||
const p = res.body?.permissions;
|
return String(res.body?.permission ?? "none");
|
||||||
if (p?.admin === true) return res.body?.owner?.login === user ? "owner" : "admin";
|
|
||||||
if (p?.push === true) return "write";
|
|
||||||
return p?.pull === true ? "read" : "none";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Make an account a collaborator of a repository that reads only. The forge asks a repository's administrator for
|
/** Make an account a collaborator of a repository that reads only. The forge asks a repository's administrator for
|
||||||
@@ -1125,38 +1067,6 @@ export class GiteaAdmin {
|
|||||||
await this.addCollaborator(owner, repo, user, "write");
|
await this.addCollaborator(owner, repo, user, "write");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The rule that applies to main in a repository, as its administrator reads it: a rule named main, else the first glob
|
|
||||||
* rule covering main; null when none does. With the administrator's login. */
|
|
||||||
async mainRule(owner: string, repo: string): Promise<BranchProtection | null> {
|
|
||||||
const rules = await this.branchProtections(owner, repo);
|
|
||||||
return rules.find((r) => (r.rule_name ?? r.branch_name) === MAIN_BRANCH)
|
|
||||||
?? rules.find((r) => ruleCovers(String(r.rule_name ?? r.branch_name ?? ""), MAIN_BRANCH)) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Put an account on main's merge whitelist in a repository of a user, and nothing else (the operator, 2026-10-26:
|
|
||||||
* write, the rule untouched). Only an existing rule whose merge whitelist is on is changed, and only its whitelist: the
|
|
||||||
* account is added to the logins already there. Push, status checks, approvals and the administrator override stay as
|
|
||||||
* the operator set them; with no rule, or the whitelist off, nothing is written (whoever writes may merge). Read back:
|
|
||||||
* the forge drops a whitelist login that cannot write the repository and still answers success, so a whitelist read back
|
|
||||||
* without the account fails loud. Answers the rule as the forge holds it after. With the administrator's login. */
|
|
||||||
async ensureMergerListed(owner: string, repo: string, account: string): Promise<{ changed: boolean; rule: BranchProtection | null }> {
|
|
||||||
const rule = await this.mainRule(owner, repo);
|
|
||||||
const listed = rule?.merge_whitelist_usernames ?? [];
|
|
||||||
if (!rule || rule.enable_merge_whitelist !== true || listed.includes(account)) return { changed: false, rule };
|
|
||||||
const name = String(rule.rule_name ?? rule.branch_name);
|
|
||||||
const path = `${repoPath(owner, repo)}/branch_protections/${encodeURIComponent(name)}`;
|
|
||||||
const res = await this.request(path, { method: "PATCH", body: JSON.stringify({ merge_whitelist_usernames: [...listed, account] }) });
|
|
||||||
if (res.status !== 200) GiteaAdmin.fail(path, res);
|
|
||||||
const after = (await this.branchProtections(owner, repo)).find((r) => (r.rule_name ?? r.branch_name) === name);
|
|
||||||
if (!after) throw new Error(`main's rule ${name} of ${owner}/${repo} was written, but the forge does not list it`);
|
|
||||||
const held = after.merge_whitelist_usernames ?? [];
|
|
||||||
if (after.enable_merge_whitelist === true && !held.includes(account)) {
|
|
||||||
throw new Error(`${account} was added to the merge whitelist of main's rule ${name} of ${owner}/${repo}, but the forge ` +
|
|
||||||
`holds [${held.join(", ")}]: it drops a login that cannot write the repository`);
|
|
||||||
}
|
|
||||||
return { changed: true, rule: after };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async addCollaborator(owner: string, repo: string, user: string, permission: "read" | "write"): Promise<void> {
|
private async addCollaborator(owner: string, repo: string, user: string, permission: "read" | "write"): Promise<void> {
|
||||||
const path = `${repoPath(owner, repo)}/collaborators/${encodeURIComponent(user)}`;
|
const path = `${repoPath(owner, repo)}/collaborators/${encodeURIComponent(user)}`;
|
||||||
const res = await this.request(path, { method: "PUT", body: JSON.stringify({ permission }) });
|
const res = await this.request(path, { method: "PUT", body: JSON.stringify({ permission }) });
|
||||||
@@ -1282,9 +1192,9 @@ export class GiteaAdmin {
|
|||||||
* other answer rejects (novox/hq issue 120).
|
* other answer rejects (novox/hq issue 120).
|
||||||
*/
|
*/
|
||||||
async holdsTeamMember(org: string, team: string, username: string, password: string): Promise<boolean> {
|
async holdsTeamMember(org: string, team: string, username: string, password: string): Promise<boolean> {
|
||||||
const me = await timedFetch(`${this.baseUrl}/api/v1/user`, {
|
const me = await fetch(`${this.baseUrl}/api/v1/user`, {
|
||||||
headers: { Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64") },
|
headers: { Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64") },
|
||||||
}, "GET /user", FORGE_REQUEST_TIMEOUT_MS);
|
});
|
||||||
if (me.status === 401 || me.status === 403) return false;
|
if (me.status === 401 || me.status === 403) return false;
|
||||||
if (me.status !== 200) throw new Error(`Gitea GET /user as ${username}: ${me.status}`);
|
if (me.status !== 200) throw new Error(`Gitea GET /user as ${username}: ${me.status}`);
|
||||||
const teams = await this.request(`/orgs/${encodeURIComponent(org)}/teams?limit=50`);
|
const teams = await this.request(`/orgs/${encodeURIComponent(org)}/teams?limit=50`);
|
||||||
|
|||||||
@@ -183,15 +183,6 @@ test("named paths: each asked path's git object id at the commit, a file's or a
|
|||||||
});
|
});
|
||||||
assert.deepEqual(got.trees, { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") });
|
assert.deepEqual(got.trees, { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") });
|
||||||
assert.equal((await client.commitTrees("novox", "hq", COMMIT)).paths, undefined, "no paths asked, none answered");
|
assert.equal((await client.commitTrees("novox", "hq", COMMIT)).paths, undefined, "no paths asked, none answered");
|
||||||
// Listed directories (ADR 0549 §2, issue 1126): the files directly under each, not deeper; a missing one lists none.
|
|
||||||
const listed = await client.commitTrees("novox", "hq", COMMIT, undefined, ["modules/a", "modules/b", "", "placement"]);
|
|
||||||
assert.deepEqual(listed.listed, {
|
|
||||||
"modules/a": { "main.go": h("6"), "module.json": h("5") },
|
|
||||||
"modules/b": {},
|
|
||||||
"": { "README.md": h("1"), "module.json": h("2") },
|
|
||||||
placement: {},
|
|
||||||
});
|
|
||||||
assert.equal((await client.commitTrees("novox", "hq", COMMIT)).listed, undefined, "no directory asked, none listed");
|
|
||||||
} finally {
|
} finally {
|
||||||
server.close();
|
server.close();
|
||||||
}
|
}
|
||||||
@@ -218,12 +209,7 @@ test("gitea_commit_trees takes paths as {directory: [paths]} and refuses another
|
|||||||
const { client, server, asked } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
|
const { client, server, asked } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
|
||||||
try {
|
try {
|
||||||
const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!;
|
const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!;
|
||||||
assert.ok("paths" in tool.input && "listed" in tool.input, "the tool names the arguments");
|
assert.ok("paths" in tool.input, "the tool names the argument");
|
||||||
const listedAnswer: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, listed: ["modules/d"] } as any);
|
|
||||||
assert.deepEqual(listedAnswer.listed, { "modules/d": { "notes.md": h("b") } });
|
|
||||||
for (const bad of ["modules/d", ["../x"], ["/abs"], [1]]) {
|
|
||||||
await assert.rejects(tool.run({ owner: "novox", repo: "hq", sha: COMMIT, listed: bad } as any), /listed/, JSON.stringify(bad));
|
|
||||||
}
|
|
||||||
const got: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, paths: { "modules/a": ["main.go"] } } as any);
|
const got: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, paths: { "modules/a": ["main.go"] } } as any);
|
||||||
assert.deepEqual(got.paths, { "modules/a": { "main.go": h("6") } });
|
assert.deepEqual(got.paths, { "modules/a": { "main.go": h("6") } });
|
||||||
const before = asked.length;
|
const before = asked.length;
|
||||||
|
|||||||
@@ -640,7 +640,6 @@ test("no tool's caller text becomes another path or another user: `..` and `?sud
|
|||||||
await tool("gitea_delete_branch").run({ ...repo, branch: "feat/x" });
|
await tool("gitea_delete_branch").run({ ...repo, branch: "feat/x" });
|
||||||
await tool("gitea_get_file").run({ ...repo, path: "modules/gitea/module.json" }).catch(() => undefined);
|
await tool("gitea_get_file").run({ ...repo, path: "modules/gitea/module.json" }).catch(() => undefined);
|
||||||
assert.deepEqual(state.asked, ["DELETE /repos/novox/mesh-catalog/branches/feat/x",
|
assert.deepEqual(state.asked, ["DELETE /repos/novox/mesh-catalog/branches/feat/x",
|
||||||
"GET /repos/novox/mesh-catalog/contents/modules/gitea", // the file's size first (issue 1577)
|
|
||||||
"GET /repos/novox/mesh-catalog/contents/modules/gitea/module.json"]);
|
"GET /repos/novox/mesh-catalog/contents/modules/gitea/module.json"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -90,48 +90,3 @@ test("a pass pages until it reaches back past its floor, and no further", async
|
|||||||
server.close();
|
server.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// **One forge request never holds the others, and a file too big for an answer is not downloaded** (novox/hq issue 1577): a
|
|
||||||
// request for a 4.9 MB file never ended and every read through the module timed out behind it.
|
|
||||||
test("a request the forge never answers is cut at the limit and says which request", async () => {
|
|
||||||
const { GiteaClient } = await import("../client.ts");
|
|
||||||
const server = createServer((req) => { if (!req.url?.includes("fast")) return; });
|
|
||||||
await new Promise<void>((r) => server.listen(0, r));
|
|
||||||
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
|
|
||||||
client.requestTimeoutMs = 150;
|
|
||||||
try {
|
|
||||||
const started = Date.now();
|
|
||||||
await assert.rejects(client.api("/repos/novox/hq/pulls"), /Gitea API GET \/repos\/novox\/hq\/pulls: no answer in 0\.15 s/);
|
|
||||||
assert.ok(Date.now() - started < 3000);
|
|
||||||
} finally {
|
|
||||||
server.closeAllConnections();
|
|
||||||
await new Promise<void>((r) => server.close(() => r()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a file over 512 KiB is refused from its directory listing, and its body is never asked for", async () => {
|
|
||||||
const { GiteaClient } = await import("../client.ts");
|
|
||||||
const asked: string[] = [];
|
|
||||||
const server = createServer((req, res) => {
|
|
||||||
asked.push(req.url ?? "");
|
|
||||||
res.setHeader("Content-Type", "application/json");
|
|
||||||
if (req.url?.startsWith("/api/v1/repos/novox/hq/contents/bin?")) {
|
|
||||||
return res.end(JSON.stringify([{ path: "bin/big", type: "file", size: 5_000_000 }, { path: "bin/small", type: "file", size: 5 }]));
|
|
||||||
}
|
|
||||||
if (req.url?.startsWith("/api/v1/repos/novox/hq/contents/bin/small")) {
|
|
||||||
return res.end(JSON.stringify({ type: "file", encoding: "base64", sha: "s", size: 5, content: Buffer.from("hello").toString("base64") }));
|
|
||||||
}
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end("{}");
|
|
||||||
});
|
|
||||||
await new Promise<void>((r) => server.listen(0, r));
|
|
||||||
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
|
|
||||||
try {
|
|
||||||
await assert.rejects(client.getFile("novox", "hq", "bin/big", "main"),
|
|
||||||
/the file is 5000000 bytes, over the 512 KiB a tool answer can carry; read it from a clone/);
|
|
||||||
assert.ok(!asked.some((u) => u.includes("/contents/bin/big")), asked.join("\n"));
|
|
||||||
assert.equal((await client.getFile("novox", "hq", "bin/small", "main")).content, "hello");
|
|
||||||
} finally {
|
|
||||||
await new Promise<void>((r) => server.close(() => r()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ interface Team { id: number; name: string; units: any; members: Set<string> }
|
|||||||
|
|
||||||
interface ForgeRepo { owner: string; name: string; full: string; private: boolean; archived?: boolean; ownerVisibility: string; collaborators: Map<string, string> }
|
interface ForgeRepo { owner: string; name: string; full: string; private: boolean; archived?: boolean; ownerVisibility: string; collaborators: Map<string, string> }
|
||||||
const forgeRepos: ForgeRepo[] = [];
|
const forgeRepos: ForgeRepo[] = [];
|
||||||
const userRules = new Map<string, any>();
|
|
||||||
const repoFaults = { refuse: new Set<string>(), permission: new Set<string>() };
|
const repoFaults = { refuse: new Set<string>(), permission: new Set<string>() };
|
||||||
function addRepo(full: string, isPrivate: boolean, ownerVisibility = "public"): ForgeRepo {
|
function addRepo(full: string, isPrivate: boolean, ownerVisibility = "public"): ForgeRepo {
|
||||||
const [owner, name] = full.split("/");
|
const [owner, name] = full.split("/");
|
||||||
@@ -85,7 +84,7 @@ function fakeForge() {
|
|||||||
const server = createServer(async (req, res) => {
|
const server = createServer(async (req, res) => {
|
||||||
const url = new URL(req.url ?? "/", "http://fake");
|
const url = new URL(req.url ?? "/", "http://fake");
|
||||||
const p = url.pathname.replace(/^\/api\/v1/, "");
|
const p = url.pathname.replace(/^\/api\/v1/, "");
|
||||||
const me = url.searchParams.has("sudo") && isAdmin(who(req)) ? url.searchParams.get("sudo") : who(req);
|
const me = who(req);
|
||||||
calls.push(`${req.method} ${p} as ${me}`);
|
calls.push(`${req.method} ${p} as ${me}`);
|
||||||
if (me === null) return json(res, 401, { message: "unauthorized" });
|
if (me === null) return json(res, 401, { message: "unauthorized" });
|
||||||
|
|
||||||
@@ -169,23 +168,6 @@ function fakeForge() {
|
|||||||
if (m[2] === "feat/unguarded") return json(res, 200, { name: m[2], protected: false });
|
if (m[2] === "feat/unguarded") return json(res, 200, { name: m[2], protected: false });
|
||||||
return json(res, 404, { message: "branch does not exist" });
|
return json(res, 404, { message: "branch does not exist" });
|
||||||
}
|
}
|
||||||
// A repository of a user: its rules are its administrator's (a site administrator, or a collaborator with admin).
|
|
||||||
m = p.match(/^\/repos\/(?!novox\/)([^/]+)\/([^/]+)\/branch_protections(?:\/(.*))?$/);
|
|
||||||
if (m) {
|
|
||||||
const key = `${m[1]}/${m[2]}`;
|
|
||||||
const repo = forgeRepos.find((r) => r.full === key);
|
|
||||||
if (!repo || !["admin", "owner"].includes(permissionOf(me, repo))) return json(res, 403, { message: "user should be an owner or a collaborator with admin write of a repository" });
|
|
||||||
if (req.method === "GET" && m[3] === undefined) return json(res, 200, userRules.has(key) ? [userRules.get(key)] : []);
|
|
||||||
// As gitea does: a whitelist login that cannot write the repository is dropped, and the write still answered.
|
|
||||||
const keep = (b: any) => (Array.isArray(b.merge_whitelist_usernames)
|
|
||||||
? { ...b, merge_whitelist_usernames: b.merge_whitelist_usernames.filter((u: string) => ["write", "admin", "owner"].includes(permissionOf(u, repo))) } : b);
|
|
||||||
if (req.method === "PATCH" && m[3] === "main") {
|
|
||||||
if (!userRules.has(key)) return json(res, 404, { message: "branch protection does not exist" });
|
|
||||||
Object.assign(userRules.get(key), keep(await read(req)));
|
|
||||||
return json(res, 200, userRules.get(key));
|
|
||||||
}
|
|
||||||
if (req.method === "POST") { const b = keep(await read(req)); userRules.set(key, b); return json(res, 201, b); }
|
|
||||||
}
|
|
||||||
// gitea's reqAdmin on the whole group: reading the rules is administration too.
|
// gitea's reqAdmin on the whole group: reading the rules is administration too.
|
||||||
m = p.match(/^\/repos\/novox\/([^/]+)\/branch_protections(?:\/(.*))?$/);
|
m = p.match(/^\/repos\/novox\/([^/]+)\/branch_protections(?:\/(.*))?$/);
|
||||||
if (m) {
|
if (m) {
|
||||||
@@ -223,7 +205,6 @@ function fakeForge() {
|
|||||||
m = p.match(/^\/repos\/([^/]+)\/([^/]+)(?:\/collaborators\/([^/]+)(\/permission)?)?$/);
|
m = p.match(/^\/repos\/([^/]+)\/([^/]+)(?:\/collaborators\/([^/]+)(\/permission)?)?$/);
|
||||||
if (m) {
|
if (m) {
|
||||||
const repo = forgeRepos.find((r) => r.owner === decodeURIComponent(m![1]) && r.name === decodeURIComponent(m![2]));
|
const repo = forgeRepos.find((r) => r.owner === decodeURIComponent(m![1]) && r.name === decodeURIComponent(m![2]));
|
||||||
if (repo && req.method === "GET" && !m[3] && url.searchParams.has("sudo") && repoFaults.permission.has(repo.full)) return json(res, 500, { message: "the forge is not well" });
|
|
||||||
if (!repo || permissionOf(me, repo) === "none") return json(res, 404, { message: "repository does not exist" });
|
if (!repo || permissionOf(me, repo) === "none") return json(res, 404, { message: "repository does not exist" });
|
||||||
if (req.method === "GET" && !m[3]) {
|
if (req.method === "GET" && !m[3]) {
|
||||||
const mine = permissionOf(me, repo);
|
const mine = permissionOf(me, repo);
|
||||||
@@ -233,8 +214,7 @@ function fakeForge() {
|
|||||||
if (req.method === "GET" && m[4]) {
|
if (req.method === "GET" && m[4]) {
|
||||||
if (repoFaults.permission.has(repo.full)) return json(res, 500, { message: "the forge is not well" });
|
if (repoFaults.permission.has(repo.full)) return json(res, 500, { message: "the forge is not well" });
|
||||||
if (!isAdmin(me) && me !== user) return json(res, 403, { message: "only the user or an administrator" });
|
if (!isAdmin(me) && me !== user) return json(res, 403, { message: "only the user or an administrator" });
|
||||||
// As gitea answers: the collaborator's own permission, not what a team gives the account.
|
return json(res, 200, { permission: permissionOf(user, repo) });
|
||||||
return json(res, 200, { permission: repo.collaborators.get(user) ?? "read" });
|
|
||||||
}
|
}
|
||||||
if (req.method === "PUT" && !m[4]) {
|
if (req.method === "PUT" && !m[4]) {
|
||||||
// gitea's reqAdmin: the repository's administration, which on a repository of another user the site administrator alone holds.
|
// gitea's reqAdmin: the repository's administration, which on a repository of another user the site administrator alone holds.
|
||||||
@@ -628,8 +608,7 @@ test("the merging account is given write on each repository the indexer's module
|
|||||||
await writes.take({ key: "car-hunter-2", op: "put", value: { v: 1, repository: "jschoubben/car-hunter" } });
|
await writes.take({ key: "car-hunter-2", op: "put", value: { v: 1, repository: "jschoubben/car-hunter" } });
|
||||||
assert.equal(forge.calls.length, from, "a repository already written asks nothing more");
|
assert.equal(forge.calls.length, from, "a repository already written asks nothing more");
|
||||||
const repo = (full: string) => forgeRepos.find((x) => x.full === full)!;
|
const repo = (full: string) => forgeRepos.find((x) => x.full === full)!;
|
||||||
assert.equal(repo("jschoubben/car-hunter").collaborators.get(TOOLS_ACCOUNT), "write", "a user's repository is written, not administered");
|
assert.equal(repo("jschoubben/car-hunter").collaborators.get(TOOLS_ACCOUNT), "write");
|
||||||
assert.equal(userRules.has("jschoubben/car-hunter"), false, "a repository with no rule for main is given none");
|
|
||||||
assert.equal(repo("novox/hq").collaborators.size, 0, "the organisation's repository is written through its team, no collaborator");
|
assert.equal(repo("novox/hq").collaborators.size, 0, "the organisation's repository is written through its team, no collaborator");
|
||||||
assert.equal((await client.getRepo("jschoubben", "car-hunter")).writes, true);
|
assert.equal((await client.getRepo("jschoubben", "car-hunter")).writes, true);
|
||||||
assert.deepEqual(said, [{ type: WRITABLE_EVENT, body: { owner: "jschoubben", repo: "car-hunter", account: TOOLS_ACCOUNT } }]);
|
assert.deepEqual(said, [{ type: WRITABLE_EVENT, body: { owner: "jschoubben", repo: "car-hunter", account: TOOLS_ACCOUNT } }]);
|
||||||
@@ -657,37 +636,9 @@ test("the merging account is given write on each repository the indexer's module
|
|||||||
assert.ok(!forge.calls.slice(puts).some((c) => c.startsWith("PUT ")), "no write is asked on a repository no module builds from");
|
assert.ok(!forge.calls.slice(puts).some((c) => c.startsWith("PUT ")), "no write is asked on a repository no module builds from");
|
||||||
assert.equal(repo("jschoubben/dotfiles").collaborators.get(TOOLS_ACCOUNT), "read");
|
assert.equal(repo("jschoubben/dotfiles").collaborators.get(TOOLS_ACCOUNT), "read");
|
||||||
repoFaults.refuse.clear();
|
repoFaults.refuse.clear();
|
||||||
// The operator's rule is left as set (the operator, 2026-10-26: "Write, rule untouched"): only mesh-tools is added to its
|
|
||||||
// merge whitelist; a status the mesh does not set and required approvals are said as blocking the mesh's merge, unchanged.
|
|
||||||
const operators = { rule_name: "main", enable_push: true, enable_merge_whitelist: true, merge_whitelist_usernames: ["jschoubben"],
|
|
||||||
enable_status_check: true, status_check_contexts: ["mesh/check", "mesh/merge-gate", "ci/external"], required_approvals: 2,
|
|
||||||
block_admin_merge_override: false, require_signed_commits: true, block_on_outdated_branch: true, block_on_rejected_reviews: true };
|
|
||||||
userRules.set("jschoubben/snake", structuredClone(operators));
|
|
||||||
const after = await grantTool.run({ repositories: ["jschoubben/snake"] }) as any;
|
const after = await grantTool.run({ repositories: ["jschoubben/snake"] }) as any;
|
||||||
assert.deepEqual(after.granted, ["jschoubben/snake"]);
|
assert.deepEqual(after.granted, ["jschoubben/snake"]);
|
||||||
assert.equal(repo("jschoubben/snake").collaborators.get(TOOLS_ACCOUNT), "write");
|
assert.deepEqual(((await unwritableTool.run({})) as any).unwritable, []);
|
||||||
assert.deepEqual(userRules.get("jschoubben/snake"), { ...operators, merge_whitelist_usernames: ["jschoubben", TOOLS_ACCOUNT] }, "only the whitelist changed");
|
|
||||||
assert.deepEqual(after.merge_whitelist, [{ repository: "jschoubben/snake", changed: true, listed: ["jschoubben", TOOLS_ACCOUNT] }]);
|
|
||||||
const blockedWhy = "writable, but main's rule blocks the mesh's merge: it requires the status ci/external, which the mesh does not set; " +
|
|
||||||
"it requires 2 approving reviews on the forge, which the gate does not give; it requires signed commits, which the mesh does not make; " +
|
|
||||||
"it refuses a branch behind main, which the mesh does not bring up to date; " +
|
|
||||||
"it refuses a merge after a review asked for changes, which the gate does not clear";
|
|
||||||
assert.deepEqual(after.merge_blocked, [{ repository: "jschoubben/snake", why: blockedWhy }]);
|
|
||||||
const again = await unwritableTool.run({}) as any;
|
|
||||||
assert.deepEqual([again.unwritable, again.merge_blocked], [[], [{ repository: "jschoubben/snake", why: blockedWhy }]]);
|
|
||||||
const writesBefore = forge.calls.filter((c) => /^PATCH .*branch_protections/.test(c)).length;
|
|
||||||
assert.deepEqual(((await grantTool.run({ repositories: ["jschoubben/snake"] })) as any).merge_whitelist[0].changed, false);
|
|
||||||
assert.equal(forge.calls.filter((c) => /^PATCH .*branch_protections/.test(c)).length, writesBefore, "a listed account is not written again");
|
|
||||||
// The mesh's own statuses never block: a rule requiring only them, with mesh-tools listed, is not reported.
|
|
||||||
userRules.set("jschoubben/snake", { rule_name: "main", enable_merge_whitelist: true, merge_whitelist_usernames: [TOOLS_ACCOUNT],
|
|
||||||
enable_status_check: true, status_check_contexts: ["mesh/check", "mesh/merge-gate", "mesh/repo-check"] });
|
|
||||||
assert.deepEqual(((await unwritableTool.run({})) as any).merge_blocked, []);
|
|
||||||
assert.deepEqual(((await grantTool.run({ repositories: ["jschoubben/snake"] })) as any).merge_blocked, []);
|
|
||||||
// The forge drops a whitelist login that cannot write and still answers 200: the whitelist is read back and that fails loud.
|
|
||||||
userRules.set("jschoubben/dotfiles", { rule_name: "main", enable_merge_whitelist: true, merge_whitelist_usernames: ["jschoubben"] });
|
|
||||||
await assert.rejects(GiteaAdmin.fromEnv(env).ensureMergerListed("jschoubben", "dotfiles", TOOLS_ACCOUNT),
|
|
||||||
/mesh-tools was added to the merge whitelist of main's rule main of jschoubben\/dotfiles, but the forge holds \[jschoubben\]: it drops a login that cannot write/);
|
|
||||||
userRules.delete("jschoubben/dotfiles");
|
|
||||||
// A module gone from the indexer leaves its repository out of the delivered list.
|
// A module gone from the indexer leaves its repository out of the delivered list.
|
||||||
await writes.take({ key: "snake", op: "delete" });
|
await writes.take({ key: "snake", op: "delete" });
|
||||||
assert.deepEqual(writes.delivered(), ["jschoubben/car-hunter", "novox/hq"]);
|
assert.deepEqual(writes.delivered(), ["jschoubben/car-hunter", "novox/hq"]);
|
||||||
|
|||||||
@@ -76,17 +76,6 @@ function namedPaths(raw: unknown): Record<string, string[]> | undefined {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The `listed` argument of gitea_commit_trees: [directory], each plain (no `.` or `..` segment, not absolute, the root
|
|
||||||
* the empty name); anything else is refused before the forge is asked. Absent is absent. */
|
|
||||||
function listedDirs(raw: unknown): string[] | undefined {
|
|
||||||
if (raw === undefined || raw === null) return undefined;
|
|
||||||
if (!Array.isArray(raw) || raw.some((d) => typeof d !== "string")) throw new Error("listed is [directory, ...]");
|
|
||||||
for (const d of raw as string[]) {
|
|
||||||
if (d !== "" && d.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) throw new Error(`listed: ${JSON.stringify(d)} is not a plain relative directory`);
|
|
||||||
}
|
|
||||||
return raw as string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin): ToolDefinition[] {
|
export function getGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin): ToolDefinition[] {
|
||||||
return namesChecked(allGiteaTools(gitea, admin));
|
return namesChecked(allGiteaTools(gitea, admin));
|
||||||
}
|
}
|
||||||
@@ -385,7 +374,7 @@ function allGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin): ToolDefini
|
|||||||
// ---- Contents and branches ----
|
// ---- Contents and branches ----
|
||||||
{
|
{
|
||||||
name: "gitea_get_file",
|
name: "gitea_get_file",
|
||||||
description: "One file's contents from a repository, decoded, at a branch, tag or commit (default the repository's default branch). Refuses a file over 512 KiB, which a tool answer cannot carry: read it from a clone.",
|
description: "One file's contents from a repository, decoded, at a branch, tag or commit (default the repository's default branch).",
|
||||||
input: {
|
input: {
|
||||||
owner: { type: "string", description: "the repository owner" },
|
owner: { type: "string", description: "the repository owner" },
|
||||||
repo: { type: "string", description: "the repository name" },
|
repo: { type: "string", description: "the repository name" },
|
||||||
@@ -561,20 +550,19 @@ function allGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin): ToolDefini
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "gitea_commit_trees",
|
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. With listed, [directory], the answer also carries listed, {directory: {name: git blob id}} of the files directly under each (the catalogue's placement files, novox/hq ADR 0549 §2). 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 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: {
|
input: {
|
||||||
owner: { type: "string", description: "the repository owner" },
|
owner: { type: "string", description: "the repository owner" },
|
||||||
repo: { type: "string", description: "the repository name" },
|
repo: { type: "string", description: "the repository name" },
|
||||||
sha: { type: "string", description: "the commit, its full 40-hex sha" },
|
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" },
|
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" },
|
||||||
listed: { type: "array", items: { type: "string" }, description: "optional, [directory]: the files directly under each, {directory: {name: git blob id}}, are answered under listed; a directory the commit lacks lists none" },
|
|
||||||
},
|
},
|
||||||
run: async (args) => {
|
run: async (args) => {
|
||||||
const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim();
|
const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim();
|
||||||
const sha = String(args.sha ?? "").trim();
|
const sha = String(args.sha ?? "").trim();
|
||||||
if (!owner || !repo) throw new Error("name the repository: owner and repo");
|
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");
|
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), listedDirs(args.listed));
|
return gitea.commitTrees(owner, repo, sha, namedPaths(args.paths));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+26
-101
@@ -18,18 +18,10 @@
|
|||||||
// `repo.writable` {owner, repo, account}: mesh-delivery-gate, which does not ask the operator's Approve of a pull request
|
// `repo.writable` {owner, repo, account}: mesh-delivery-gate, which does not ask the operator's Approve of a pull request
|
||||||
// whose merge the forge would refuse, reads that repository again on it.
|
// whose merge the forge would refuse, reads that repository again on it.
|
||||||
//
|
//
|
||||||
// **A repository of a user is written, and main's rule is left as the operator set it** (the operator, 2026-10-26, asked
|
|
||||||
// "Write, rule untouched"): the account is a write collaborator there, and only added to main's merge whitelist when that
|
|
||||||
// rule keeps one (client.ts ensureMergerListed). What else in main's rule stops the mesh's merge (a status the mesh does not
|
|
||||||
// set, approvals the gate does not give) is changed by nobody but said: both tools list such a repository as writable, but
|
|
||||||
// blocked, with why (merge_blocked).
|
|
||||||
//
|
|
||||||
// Not taken away: a repository whose last module goes from the indexer keeps its collaborator until a hand removes it;
|
// Not taken away: a repository whose last module goes from the indexer keeps its collaborator until a hand removes it;
|
||||||
// gitea_unwritable_repos and gitea_grant_write say the delivered list as it is now.
|
// gitea_unwritable_repos and gitea_grant_write say the delivered list as it is now.
|
||||||
|
|
||||||
import type { ForgeRepository } from "./client.js";
|
import type { ForgeRepository } from "./client.js";
|
||||||
import type { BranchProtection } from "./protection.js";
|
|
||||||
import { CHECK_CONTEXT, CHECK_STATUS, REPO_CHECK_CONTEXT } from "./pulls.js";
|
|
||||||
|
|
||||||
/** The indexer's state this module reads: one key per registered module, its value naming the repository. */
|
/** The indexer's state this module reads: one key per registered module, its value naming the repository. */
|
||||||
export const INDEXER_MODULES = "mesh-delivery-indexer.modules";
|
export const INDEXER_MODULES = "mesh-delivery-indexer.modules";
|
||||||
@@ -42,35 +34,8 @@ export interface WriteForge {
|
|||||||
repository(owner: string, repo: string): Promise<ForgeRepository | null>;
|
repository(owner: string, repo: string): Promise<ForgeRepository | null>;
|
||||||
repositoryPermission(owner: string, repo: string, user: string): Promise<string>;
|
repositoryPermission(owner: string, repo: string, user: string): Promise<string>;
|
||||||
addWriteCollaborator(owner: string, repo: string, user: string): Promise<void>;
|
addWriteCollaborator(owner: string, repo: string, user: string): Promise<void>;
|
||||||
mainRule(owner: string, repo: string): Promise<BranchProtection | null>;
|
|
||||||
ensureMergerListed(owner: string, repo: string, account: string): Promise<{ changed: boolean; rule: BranchProtection | null }>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The statuses every pull request the mesh delivers carries (mesh-delivery's checks); a rule requiring another blocks. */
|
|
||||||
const MESH_CONTEXTS = new Set([CHECK_STATUS, CHECK_CONTEXT, REPO_CHECK_CONTEXT]);
|
|
||||||
|
|
||||||
/** What in main's rule stops the mesh's merge, in words; empty when nothing does. Changed by nobody here: the operator's. */
|
|
||||||
export function mergeBlocks(rule: BranchProtection | null, account: string): string[] {
|
|
||||||
if (!rule) return [];
|
|
||||||
const blocks: string[] = [];
|
|
||||||
if (rule.enable_merge_whitelist === true && !(rule.merge_whitelist_usernames ?? []).includes(account)) {
|
|
||||||
blocks.push(`its merge whitelist [${(rule.merge_whitelist_usernames ?? []).join(", ")}] leaves out ${account} (gitea_grant_write adds it)`);
|
|
||||||
}
|
|
||||||
if (rule.enable_status_check === true) {
|
|
||||||
for (const c of rule.status_check_contexts ?? []) {
|
|
||||||
if (!MESH_CONTEXTS.has(c)) blocks.push(`it requires the status ${c}, which the mesh does not set`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const approvals = Number(rule.required_approvals ?? 0);
|
|
||||||
if (approvals > 0) blocks.push(`it requires ${approvals} approving review${approvals === 1 ? "" : "s"} on the forge, which the gate does not give`);
|
|
||||||
if (rule.require_signed_commits === true) blocks.push("it requires signed commits, which the mesh does not make");
|
|
||||||
if (rule.block_on_outdated_branch === true) blocks.push("it refuses a branch behind main, which the mesh does not bring up to date");
|
|
||||||
if (rule.block_on_rejected_reviews === true) blocks.push("it refuses a merge after a review asked for changes, which the gate does not clear");
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockedWhy = (blocks: string[]): string => `writable, but main's rule blocks the mesh's merge: ${blocks.join("; ")}`;
|
|
||||||
|
|
||||||
/** One change of the indexer's modules state, as the SDK's state watch hands it. */
|
/** One change of the indexer's modules state, as the SDK's state watch hands it. */
|
||||||
export interface ModulesChange {
|
export interface ModulesChange {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -86,13 +51,8 @@ export interface Written {
|
|||||||
delivered: string[];
|
delivered: string[];
|
||||||
/** Made writable now, each read back from the forge. */
|
/** Made writable now, each read back from the forge. */
|
||||||
granted: string[];
|
granted: string[];
|
||||||
/** Written already (a collaborator of a user's repository), or written through the organisation's team. */
|
/** Written already (the organisation's team, or a collaborator that writes). */
|
||||||
already: string[];
|
already: string[];
|
||||||
/** Each repository of a user: whether the account was added to main's merge whitelist now, and the whitelist read back
|
|
||||||
* (null: main's rule keeps no whitelist, or there is no rule, and whoever writes may merge). */
|
|
||||||
merge_whitelist: { repository: string; changed: boolean; listed: string[] | null }[];
|
|
||||||
/** Writable, but main's rule (the operator's, left as set) stops the mesh's merge, and why. */
|
|
||||||
merge_blocked: { repository: string; why: string }[];
|
|
||||||
/** The forge refused, in its own words; or the repository is the organisation's, whose team should write it. */
|
/** The forge refused, in its own words; or the repository is the organisation's, whose team should write it. */
|
||||||
refused: { repository: string; why: string }[];
|
refused: { repository: string; why: string }[];
|
||||||
/** Repositories the account cannot write that are left so, and why: they build no registered module. */
|
/** Repositories the account cannot write that are left so, and why: they build no registered module. */
|
||||||
@@ -158,7 +118,7 @@ export class DeliveredWrites {
|
|||||||
"(the watch is not granted or not answered); nothing is granted");
|
"(the watch is not granted or not answered); nothing is granted");
|
||||||
}
|
}
|
||||||
const delivered = this.delivered();
|
const delivered = this.delivered();
|
||||||
const result: Written = { account: this.account, delivered, granted: [], already: [], merge_whitelist: [], merge_blocked: [], refused: [], left_read_only: [] };
|
const result: Written = { account: this.account, delivered, granted: [], already: [], refused: [], left_read_only: [] };
|
||||||
const named = only && only.length ? only.map((x) => String(x).trim()) : null;
|
const named = only && only.length ? only.map((x) => String(x).trim()) : null;
|
||||||
const targets: string[] = [];
|
const targets: string[] = [];
|
||||||
for (const full of named ?? delivered) {
|
for (const full of named ?? delivered) {
|
||||||
@@ -180,55 +140,36 @@ export class DeliveredWrites {
|
|||||||
result.refused.push({ repository: full, why: `its permission could not be read — ${said(err)}` });
|
result.refused.push({ repository: full, why: `its permission could not be read — ${said(err)}` });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (WRITES.has(permission)) {
|
||||||
|
this.writable.add(full);
|
||||||
|
result.already.push(full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (owner === this.org) {
|
if (owner === this.org) {
|
||||||
if (WRITES.has(permission)) {
|
|
||||||
this.writable.add(full);
|
|
||||||
result.already.push(full);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// The organisation's repositories are written through its team (tools-account.ts), never a collaborator.
|
// The organisation's repositories are written through its team (tools-account.ts), never a collaborator.
|
||||||
result.refused.push({ repository: full, why: `a repository of ${this.org}, which ${this.account} writes through the ` +
|
result.refused.push({ repository: full, why: `a repository of ${this.org}, which ${this.account} writes through the ` +
|
||||||
`team ${this.org}/${this.account}; the forge gives it "${permission}", so the team is not as it should be (it is ` +
|
`team ${this.org}/${this.account}; the forge gives it "${permission}", so the team is not as it should be (it is ` +
|
||||||
"set again at the next mint of the tools' token)" });
|
"set again at the next mint of the tools' token)" });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// A repository of a user: the account writes it, and is put on main's merge whitelist when the rule keeps one.
|
|
||||||
let changed = false;
|
|
||||||
if (!WRITES.has(permission)) {
|
|
||||||
try {
|
|
||||||
await this.forge.addWriteCollaborator(owner, name, this.account);
|
|
||||||
} catch (err) {
|
|
||||||
result.refused.push({ repository: full, why: said(err) });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let after: string;
|
|
||||||
try {
|
|
||||||
after = await this.forge.repositoryPermission(owner, name, this.account);
|
|
||||||
} catch (err) {
|
|
||||||
result.refused.push({ repository: full, why: `granted, but its permission could not be read back — ${said(err)}` });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!WRITES.has(after)) {
|
|
||||||
result.refused.push({ repository: full, why: `the grant was answered, but the forge still gives ${this.account} "${after}"` });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const { changed: listedNow, rule } = await this.forge.ensureMergerListed(owner, name, this.account);
|
await this.forge.addWriteCollaborator(owner, name, this.account);
|
||||||
result.merge_whitelist.push({ repository: full, changed: listedNow,
|
|
||||||
listed: rule?.enable_merge_whitelist === true ? (rule.merge_whitelist_usernames ?? []) : null });
|
|
||||||
const blocks = mergeBlocks(rule, this.account);
|
|
||||||
if (blocks.length > 0) result.merge_blocked.push({ repository: full, why: blockedWhy(blocks) });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
result.refused.push({ repository: full, why: `${this.account} writes it, but could not be put on main's merge whitelist — ${said(err)}` });
|
result.refused.push({ repository: full, why: said(err) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let after: string;
|
||||||
|
try {
|
||||||
|
after = await this.forge.repositoryPermission(owner, name, this.account);
|
||||||
|
} catch (err) {
|
||||||
|
result.refused.push({ repository: full, why: `granted, but its permission could not be read back — ${said(err)}` });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!WRITES.has(after)) {
|
||||||
|
result.refused.push({ repository: full, why: `the grant was answered, but the forge still gives ${this.account} "${after}"` });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this.writable.add(full);
|
this.writable.add(full);
|
||||||
if (!changed) {
|
|
||||||
result.already.push(full);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
result.granted.push(full);
|
result.granted.push(full);
|
||||||
await this.emit(WRITABLE_EVENT, { owner, repo: name, account: this.account }).catch((err) =>
|
await this.emit(WRITABLE_EVENT, { owner, repo: name, account: this.account }).catch((err) =>
|
||||||
this.log(`${this.account} writes ${full} now, and that could not be said on the bus — ${said(err)}`));
|
this.log(`${this.account} writes ${full} now, and that could not be said on the bus — ${said(err)}`));
|
||||||
@@ -251,12 +192,10 @@ export class DeliveredWrites {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Every delivered repository the account cannot write, as the forge says now, and why. */
|
/** Every delivered repository the account cannot write, as the forge says now, and why. */
|
||||||
async unwritable(): Promise<{ account: string; delivered: number; unwritable: { repository: string; permission: string; why: string }[];
|
async unwritable(): Promise<{ account: string; delivered: number; unwritable: { repository: string; permission: string; why: string }[] }> {
|
||||||
merge_blocked: { repository: string; why: string }[] }> {
|
|
||||||
if (!this.heard) throw new Error(`which repositories build a registered module is not known yet: ${INDEXER_MODULES} has not been read`);
|
if (!this.heard) throw new Error(`which repositories build a registered module is not known yet: ${INDEXER_MODULES} has not been read`);
|
||||||
const delivered = this.delivered();
|
const delivered = this.delivered();
|
||||||
const out: { repository: string; permission: string; why: string }[] = [];
|
const out: { repository: string; permission: string; why: string }[] = [];
|
||||||
const blocked: { repository: string; why: string }[] = [];
|
|
||||||
for (const full of delivered) {
|
for (const full of delivered) {
|
||||||
const [owner, name] = split(full);
|
const [owner, name] = split(full);
|
||||||
let permission: string;
|
let permission: string;
|
||||||
@@ -266,21 +205,12 @@ export class DeliveredWrites {
|
|||||||
out.push({ repository: full, permission: "unknown", why: `the forge did not answer ${this.account}'s permission on it — ${said(err)}` });
|
out.push({ repository: full, permission: "unknown", why: `the forge did not answer ${this.account}'s permission on it — ${said(err)}` });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (WRITES.has(permission)) {
|
if (WRITES.has(permission)) continue;
|
||||||
if (owner === this.org) continue; // the organisation's main rules are the mesh's own (mainrules.ts)
|
|
||||||
try {
|
|
||||||
const blocks = mergeBlocks(await this.forge.mainRule(owner, name), this.account);
|
|
||||||
if (blocks.length > 0) blocked.push({ repository: full, why: blockedWhy(blocks) });
|
|
||||||
} catch (err) {
|
|
||||||
blocked.push({ repository: full, why: `writable, but main's rule could not be read — ${said(err)}` });
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const modules = [...this.modules].filter(([, r]) => r === full).map(([m]) => m).sort();
|
const modules = [...this.modules].filter(([, r]) => r === full).map(([m]) => m).sort();
|
||||||
out.push({ repository: full, permission, why: `builds ${modules.join(", ")}, and the forge gives ${this.account} ` +
|
out.push({ repository: full, permission, why: `builds ${modules.join(", ")}, and the forge gives ${this.account} ` +
|
||||||
`"${permission}" on it: its commit status and its merge are refused until gitea_grant_write grants it` });
|
`"${permission}" on it: its commit status and its merge are refused until gitea_grant_write grants it` });
|
||||||
}
|
}
|
||||||
return { account: this.account, delivered: delivered.length, unwritable: out, merge_blocked: blocked };
|
return { account: this.account, delivered: delivered.length, unwritable: out };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,19 +224,14 @@ export function writeTools(writes: () => DeliveredWrites): Tool[] {
|
|||||||
description: "Every repository that builds a registered module (the indexer's modules, read over the bus) on which " +
|
description: "Every repository that builds a registered module (the indexer's modules, read over the bus) on which " +
|
||||||
"the mesh's merging forge account (mesh-tools, which sets commit statuses and merges) cannot write, with the " +
|
"the mesh's merging forge account (mesh-tools, which sets commit statuses and merges) cannot write, with the " +
|
||||||
"permission the forge gives it there and why: such a pull request can be checked but never merged, and the " +
|
"permission the forge gives it there and why: such a pull request can be checked but never merged, and the " +
|
||||||
"delivery gate does not ask its Approve. Empty when it writes every one. Its merge_blocked lists each repository of " +
|
"delivery gate does not ask its Approve. Empty when it writes every one. Read with the site administrator's login.",
|
||||||
"a user it writes whose main's rule (the operator's, left as set) still stops the mesh's merge: a merge whitelist " +
|
|
||||||
"without it, a required status the mesh does not set, required approving reviews. Read with the site administrator's login.",
|
|
||||||
input: {},
|
input: {},
|
||||||
run: async () => writes().unwritable(),
|
run: async () => writes().unwritable(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "gitea_grant_write",
|
name: "gitea_grant_write",
|
||||||
description: "Give the mesh's merging forge account (mesh-tools) write, as a collaborator, on every repository of a " +
|
description: "Give the mesh's merging forge account (mesh-tools) write, as a collaborator, on every repository that " +
|
||||||
"user that builds a registered module, or on the repositories named (each must build one). Main's rule there is left " +
|
"builds a registered module and that it cannot write now, or on the repositories named (each must build one). " +
|
||||||
"as the operator set it; only when it keeps a merge whitelist is mesh-tools added to it, read back (merge_whitelist). " +
|
|
||||||
"What else in that rule stops the mesh's merge (a required status the mesh does not set, required approving reviews) " +
|
|
||||||
"is changed by nobody and said in merge_blocked. " +
|
|
||||||
"The module does this itself at its start and when a module of a new repository is registered; this is the same " +
|
"The module does this itself at its start and when a module of a new repository is registered; this is the same " +
|
||||||
"act by hand, such as when a merge or a commit status was refused 403. A repository of the organisation novox " +
|
"act by hand, such as when a merge or a commit status was refused 403. A repository of the organisation novox " +
|
||||||
"is written through its team and needs nothing. Answers what was granted (each read back from the forge), what " +
|
"is written through its team and needs nothing. Answers what was granted (each read back from the forge), what " +
|
||||||
|
|||||||
@@ -1,9 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# the copies of this package in every module that carries one.
|
|
||||||
modules/xorg/internal/desktop/*.go
|
|
||||||
modules/lemurs/internal/desktop/*.go
|
|
||||||
modules/i3/internal/desktop/*.go
|
|
||||||
modules/xterm/internal/desktop/*.go
|
|
||||||
modules/adwaita/internal/desktop/*.go
|
|
||||||
|
|||||||
@@ -1,9 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||||
# ? 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.
|
|
||||||
# the copies of this package in every module that carries one.
|
|
||||||
modules/xorg/internal/desktop/*.go
|
|
||||||
modules/lemurs/internal/desktop/*.go
|
|
||||||
modules/i3/internal/desktop/*.go
|
|
||||||
modules/xterm/internal/desktop/*.go
|
|
||||||
modules/adwaita/internal/desktop/*.go
|
|
||||||
|
|||||||
@@ -84,17 +84,9 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
|||||||
// The capacity's own change wakes the puller (issue 1887): the runtime's watch of the config value as mesh-config
|
// The capacity's own change wakes the puller (issue 1887): the runtime's watch of the config value as mesh-config
|
||||||
// resolved it; the settings file is read only when that watch cannot start. A test's Capacity is its own.
|
// resolved it; the settings file is read only when that watch cannot start. A test's Capacity is its own.
|
||||||
var watch func(string, func(stdio.ProvisionChange) error) error
|
var watch func(string, func(stdio.ProvisionChange) error) error
|
||||||
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
|
||||||
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
|
||||||
// And the node's build hours (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00): outside them it takes
|
|
||||||
// nothing new.
|
|
||||||
var cores func() float64
|
|
||||||
var hours func() (*buildstep.BuildWindow, string)
|
|
||||||
if capacity == nil {
|
if capacity == nil {
|
||||||
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
|
||||||
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
||||||
watch = buildstep.WatchConfigServed
|
watch = buildstep.WatchConfigServed
|
||||||
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskGo], m.Say)
|
|
||||||
}
|
}
|
||||||
ask := m.askSteps(steps)
|
ask := m.askSteps(steps)
|
||||||
var load func(string) ([]byte, error)
|
var load func(string) ([]byte, error)
|
||||||
@@ -105,7 +97,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &buildstep.Puller{Source: source, Language: delivery.AskGo, Node: m.Node, Put: m.Steps, PausePut: m.Pauses, Load: load,
|
return &buildstep.Puller{Source: source, Language: delivery.AskGo, Node: m.Node, Put: m.Steps, PausePut: m.Pauses, Load: load,
|
||||||
Capacity: capacity, WatchConfig: watch, CoresPerStep: cores, BuildHours: hours, Logf: m.Say, Has: m.Has,
|
Capacity: capacity, WatchConfig: watch, Logf: m.Say, Has: m.Has,
|
||||||
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
|
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
|
||||||
if err := m.Check(ctx); err != nil {
|
if err := m.Check(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -126,28 +125,12 @@ func TestManifestClaimsTheGoBuildSeatAndReadsItsCapacity(t *testing.T) {
|
|||||||
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
||||||
var doc struct {
|
var doc struct {
|
||||||
Config map[string]struct {
|
Config map[string]struct {
|
||||||
Default json.RawMessage `json:"default"`
|
Default string `json:"default"`
|
||||||
} `json:"config"`
|
} `json:"config"`
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(raw, &doc)
|
_ = json.Unmarshal(raw, &doc)
|
||||||
// Issue 1806's follow-up: the cores a step needs is a setting of its own, in the settings file, with the SDK's default.
|
if doc.Config[buildstep.CapacitySetting].Default != buildstep.DefaultCapacities {
|
||||||
if cores := string(doc.Config[buildstep.CoresPerStepSetting].Default); cores != fmt.Sprint(buildstep.DefaultCoresPerStep[delivery.AskGo]) {
|
t.Errorf("the capacity's default = %q, want %q", doc.Config[buildstep.CapacitySetting].Default, buildstep.DefaultCapacities)
|
||||||
t.Errorf("the cores-per-step default = %s, want %g", cores, buildstep.DefaultCoresPerStep[delivery.AskGo])
|
|
||||||
}
|
|
||||||
if got := buildstep.NodeCoresPerStep(moduleDir+"/files/settings.json", 0, nil)(); got != buildstep.DefaultCoresPerStep[delivery.AskGo] {
|
|
||||||
t.Errorf("the settings template holds cores-per-step %g", got)
|
|
||||||
}
|
|
||||||
// And the build hours (the operator, 2026-10-26): a setting of their own, empty by default (any hour), in the template.
|
|
||||||
if hours := string(doc.Config[buildstep.BuildHoursSetting].Default); hours != `""` {
|
|
||||||
t.Errorf("the build-hours default = %s, want empty", hours)
|
|
||||||
}
|
|
||||||
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
|
|
||||||
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
|
|
||||||
}
|
|
||||||
var capacity string
|
|
||||||
_ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity)
|
|
||||||
if capacity != buildstep.DefaultCapacities {
|
|
||||||
t.Errorf("the capacity's default = %q, want %q", capacity, buildstep.DefaultCapacities)
|
|
||||||
}
|
}
|
||||||
settings := resource(t, m, "settings")
|
settings := resource(t, m, "settings")
|
||||||
if settings["path"] != "${dir:state}/settings.json" {
|
if settings["path"] != "${dir:state}/settings.json" {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func TestTheIntegrationLevelDigestsUnderBothTags(t *testing.T) {
|
|||||||
t.Errorf("the integration digest listed %q", s.command)
|
t.Errorf("the integration digest listed %q", s.command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if DigestVersion != "6" {
|
if DigestVersion != "5" {
|
||||||
t.Errorf("the digest's form changed in kind and is still version %s", DigestVersion)
|
t.Errorf("the digest's form changed in kind and is still version %s", DigestVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import (
|
|||||||
func (m *Module) cpuLimits() buildcpu.Limits {
|
func (m *Module) cpuLimits() buildcpu.Limits {
|
||||||
slots := 1
|
slots := 1
|
||||||
switch {
|
switch {
|
||||||
case m.Held != nil:
|
|
||||||
slots = m.Held.CapacityNow() // the capacity the puller judges by, bounded by the node's build slice
|
|
||||||
case m.Capacity != nil:
|
case m.Capacity != nil:
|
||||||
slots = m.Capacity()
|
slots = m.Capacity()
|
||||||
case m.Settings != "":
|
case m.Settings != "":
|
||||||
@@ -34,9 +32,5 @@ func (m *Module) threadEnv() []string {
|
|||||||
// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime
|
// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime
|
||||||
// to (its unit and its slice), read from its cgroup rather than assumed.
|
// to (its unit and its slice), read from its cgroup rather than assumed.
|
||||||
func (m *Module) cpuStatus() map[string]any {
|
func (m *Module) cpuStatus() map[string]any {
|
||||||
out := map[string]any{"limits": m.cpuLimits(), "in-force": buildcpu.ReadInForce()}
|
return map[string]any{"limits": m.cpuLimits(), "in-force": buildcpu.ReadInForce()}
|
||||||
if m.Held != nil {
|
|
||||||
out["capacity"] = m.Held.CapacityDetail() // the capacity judged by and why (issue 1806's follow-up)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -30,25 +29,17 @@ import (
|
|||||||
// commits (the lead's decision L5) and the integration level's tags integration,isolation (the checker's agreement).
|
// commits (the lead's decision L5) and the integration level's tags integration,isolation (the checker's agreement).
|
||||||
// 4: the siblings' commits enter only the digest of a package whose tests reach a file naming MESH_CHECK_BESIDE (ADR 0536 §1).
|
// 4: the siblings' commits enter only the digest of a package whose tests reach a file naming MESH_CHECK_BESIDE (ADR 0536 §1).
|
||||||
// 5: a catalogue module's non-Go files enter the digest of its packages (issue 1935).
|
// 5: a catalogue module's non-Go files enter the digest of its packages (issue 1935).
|
||||||
// 6: a reads-outside marker that lists what its tests read enters the digest with those files' contents (issue 1936).
|
const DigestVersion = "5"
|
||||||
const DigestVersion = "6"
|
|
||||||
|
|
||||||
// besideVariable is the name a file bears when it reads where the siblings lie; a package whose tests reach such a file
|
// besideVariable is the name a file bears when it reads where the siblings lie; a package whose tests reach such a file
|
||||||
// depends on the siblings' commits.
|
// depends on the siblings' commits.
|
||||||
const besideVariable = "MESH_CHECK_BESIDE"
|
const besideVariable = "MESH_CHECK_BESIDE"
|
||||||
|
|
||||||
// ReadsOutsideMarker is the name of a file in a package's directory that says its tests read files outside it (a
|
// ReadsOutsideMarker is the name of a file in a package's directory that says its tests read files outside it (a
|
||||||
// repository's documents, a sibling directory), which the Go toolchain does not list (issue 1935). It lists, one per
|
// repository's documents, a sibling directory): such a package is never kept (issue 1935). The digest hashes only what
|
||||||
// line, what those tests read, as paths from the repository's root; a path may be a glob (filepath.Match), and a
|
// the Go toolchain lists, so it cannot name what a test opens elsewhere; a marker file, beside the Go files where a
|
||||||
// directory it names is read whole (issue 1936). Blank lines and lines starting with # are not read. A line starting
|
// reader of the package sees it, is the plain way to say so until the checker keeps the files a test opened (the Go
|
||||||
// with ? names a path whose absence is what the tests read (a file that must not be there): it may match nothing.
|
// test log) with each verdict.
|
||||||
//
|
|
||||||
// The digest then hashes, besides what the toolchain lists, every line of the marker, every file it names (under its
|
|
||||||
// path in the tree, by content) and every file of the package's own directory, Go files included (a file its build tags
|
|
||||||
// exclude is one go list does not hash). A marker that lists nothing, a line that is not a clean relative path, or a
|
|
||||||
// line without ? that matches nothing makes the package one that is never kept (its digest a fresh random hex): a list
|
|
||||||
// nobody can trust keeps nothing. The exact fix, the files a test opened kept
|
|
||||||
// from the Go test log with each verdict, replaces the lists (issue 1936).
|
|
||||||
const ReadsOutsideMarker = "reads-outside"
|
const ReadsOutsideMarker = "reads-outside"
|
||||||
|
|
||||||
// NeedsMarker is the name of a file in a package's directory that names, one per line, what its integration tests need
|
// NeedsMarker is the name of a file in a package's directory that names, one per line, what its integration tests need
|
||||||
@@ -137,13 +128,9 @@ type DigestResult struct {
|
|||||||
// only when they are the siblings it gave the fetch.
|
// only when they are the siblings it gave the fetch.
|
||||||
Beside []string `json:"beside,omitempty"`
|
Beside []string `json:"beside,omitempty"`
|
||||||
WithoutTests []string `json:"without_tests,omitempty"`
|
WithoutTests []string `json:"without_tests,omitempty"`
|
||||||
// ReadsOutside names the packages whose marker lists nothing it can trust (ReadsOutsideMarker): each has a digest
|
// ReadsOutside names the packages that carry the marker: each has a digest that no kept verdict can have (a fresh
|
||||||
// that no kept verdict can have (a fresh random one), so the check runs it every time.
|
// random one), so the check runs it every time.
|
||||||
ReadsOutside []string `json:"reads_outside,omitempty"`
|
ReadsOutside []string `json:"reads_outside,omitempty"`
|
||||||
// neverKept is, per package of ReadsOutside, why its marker cannot be trusted; hashedFiles caches the content lines
|
|
||||||
// of the files markers name, which several packages share (the catalogue's manifests).
|
|
||||||
neverKept map[string]string
|
|
||||||
hashedFiles map[string]string
|
|
||||||
// Needs are the packages whose tests at this level need something of the node, as their NeedsMarker names it.
|
// Needs are the packages whose tests at this level need something of the node, as their NeedsMarker names it.
|
||||||
Needs []delivery.PackageNeeds `json:"needs,omitempty"`
|
Needs []delivery.PackageNeeds `json:"needs,omitempty"`
|
||||||
// JudgedBy is the program that ran the digest (ADR 0535 §5): "installed", or "candidate <merge commit>" under the
|
// JudgedBy is the program that ran the digest (ADR 0535 §5): "installed", or "candidate <merge commit>" under the
|
||||||
@@ -260,7 +247,7 @@ func (m *Module) digestHere(ctx context.Context, workspace, level string) (Diges
|
|||||||
sort.Slice(res.Needs, func(i, j int) bool { return res.Needs[i].Package < res.Needs[j].Package })
|
sort.Slice(res.Needs, func(i, j int) bool { return res.Needs[i].Package < res.Needs[j].Package })
|
||||||
for _, p := range res.ReadsOutside {
|
for _, p := range res.ReadsOutside {
|
||||||
if m.Say != nil {
|
if m.Say != nil {
|
||||||
m.Say("the %s level of %s always runs: its %s %s, so its tests read files outside it that no digest names", level, p, ReadsOutsideMarker, res.neverKept[p])
|
m.Say("the %s level of %s always runs: its directory carries %s, so its tests read files outside it that no digest names", level, p, ReadsOutsideMarker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -329,27 +316,18 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he
|
|||||||
res.Needs = append(res.Needs, delivery.PackageNeeds{Package: path, Needs: n})
|
res.Needs = append(res.Needs, delivery.PackageNeeds{Package: path, Needs: n})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
declared, why, err := declaredReads(tree, root, res.hashed())
|
if _, err := os.Stat(filepath.Join(root.Dir, ReadsOutsideMarker)); err == nil {
|
||||||
if err != nil {
|
// The digest of a package that reads outside itself is one nothing can have kept: the key is a fresh random
|
||||||
return fmt.Errorf("%s: its %s: %w", path, ReadsOutsideMarker, err)
|
// hex, so the ask finds no kept verdict and runs it, and what the checker writes under it is never read again.
|
||||||
}
|
|
||||||
if why != "" {
|
|
||||||
// The digest of a package whose reads outside itself are not listed is one nothing can have kept: the key is a
|
|
||||||
// fresh random hex, so the ask finds no kept verdict and runs it, and what the checker writes under it is never
|
|
||||||
// read again.
|
|
||||||
var nonce [32]byte
|
var nonce [32]byte
|
||||||
if _, err := rand.Read(nonce[:]); err != nil {
|
if _, err := rand.Read(nonce[:]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
res.Packages[path] = hex.EncodeToString(nonce[:])
|
res.Packages[path] = hex.EncodeToString(nonce[:])
|
||||||
res.ReadsOutside = append(res.ReadsOutside, path)
|
res.ReadsOutside = append(res.ReadsOutside, path)
|
||||||
if res.neverKept == nil {
|
|
||||||
res.neverKept = map[string]string{}
|
|
||||||
}
|
|
||||||
res.neverKept[path] = why
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
d, err := packageDigest(root, testMain, pkgs, tree, header, beside, declared)
|
d, err := packageDigest(root, testMain, pkgs, tree, header, beside)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
}
|
}
|
||||||
@@ -362,15 +340,14 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he
|
|||||||
// tests reach in the module's own tree (by their content, under their path in the tree), the version and checksum of
|
// tests reach in the module's own tree (by their content, under their path in the tree), the version and checksum of
|
||||||
// every module outside it, and the package's own test data. The beside lines (the siblings' commits) join the header
|
// every module outside it, and the package's own test data. The beside lines (the siblings' commits) join the header
|
||||||
// only when a file the tests reach names MESH_CHECK_BESIDE. A package of the standard library is the Go version's.
|
// only when a file the tests reach names MESH_CHECK_BESIDE. A package of the standard library is the Go version's.
|
||||||
// The lines a package's ReadsOutsideMarker declares join them (declaredReads).
|
func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header, beside []string) (string, error) {
|
||||||
func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header, beside, declared []string) (string, error) {
|
|
||||||
reached := map[string]*goPackage{root.ImportPath: root}
|
reached := map[string]*goPackage{root.ImportPath: root}
|
||||||
for _, dep := range testMain.Deps {
|
for _, dep := range testMain.Deps {
|
||||||
if p := pkgs[dep]; p != nil {
|
if p := pkgs[dep]; p != nil {
|
||||||
reached[dep] = p
|
reached[dep] = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines := append([]string{}, declared...)
|
lines := []string{}
|
||||||
readsBeside := false
|
readsBeside := false
|
||||||
for _, p := range reached {
|
for _, p := range reached {
|
||||||
if p.Standard || p.Module == nil {
|
if p.Standard || p.Module == nil {
|
||||||
@@ -492,104 +469,3 @@ func contentLine(tree string, p *goPackage, path string) (string, error) {
|
|||||||
sum := sha256.Sum256(raw)
|
sum := sha256.Sum256(raw)
|
||||||
return "file " + label + " " + hex.EncodeToString(sum[:]), nil
|
return "file " + label + " " + hex.EncodeToString(sum[:]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// hashed is the cache of content lines a digest step shares across its packages.
|
|
||||||
func (r *DigestResult) hashed() map[string]string {
|
|
||||||
if r.hashedFiles == nil {
|
|
||||||
r.hashedFiles = map[string]string{}
|
|
||||||
}
|
|
||||||
return r.hashedFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
// declaredReads are the digest lines a package's ReadsOutsideMarker adds: each of its lines, the content line of every
|
|
||||||
// file it names, and those of every file of the package's own directory. Without a marker it answers nothing. why is
|
|
||||||
// set, and the lines nil, when the marker cannot be trusted: it lists nothing, a line is not a clean relative path, or
|
|
||||||
// a line without ? matches nothing.
|
|
||||||
func declaredReads(tree string, root *goPackage, cache map[string]string) (lines []string, why string, err error) {
|
|
||||||
raw, err := os.ReadFile(filepath.Join(root.Dir, ReadsOutsideMarker))
|
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return nil, "", nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
var patterns []string
|
|
||||||
for _, l := range strings.Split(string(raw), "\n") {
|
|
||||||
if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "#") {
|
|
||||||
patterns = append(patterns, l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(patterns) == 0 {
|
|
||||||
return nil, "lists nothing", nil
|
|
||||||
}
|
|
||||||
seen := map[string]bool{}
|
|
||||||
add := func(path string) error {
|
|
||||||
if seen[path] {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
seen[path] = true
|
|
||||||
l, ok := cache[path]
|
|
||||||
if !ok {
|
|
||||||
var err error
|
|
||||||
if l, err = contentLine(tree, root, path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cache[path] = l
|
|
||||||
}
|
|
||||||
lines = append(lines, "read "+l)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, l := range patterns {
|
|
||||||
pattern, absent := strings.CutPrefix(l, "?")
|
|
||||||
if !cleanTreePattern(pattern) {
|
|
||||||
return nil, fmt.Sprintf("names %q, which is not a clean path from the repository's root", l), nil
|
|
||||||
}
|
|
||||||
matches, err := filepath.Glob(filepath.Join(tree, filepath.FromSlash(pattern)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Sprintf("names %q, which is not a glob", l), nil
|
|
||||||
}
|
|
||||||
if len(matches) == 0 && !absent {
|
|
||||||
return nil, fmt.Sprintf("names %q, which matches nothing (a path whose absence is read starts with ?)", l), nil
|
|
||||||
}
|
|
||||||
lines = append(lines, "reads "+l)
|
|
||||||
for _, match := range matches {
|
|
||||||
if err := walkFiles(match, add); err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Every file of the package's own directory: its lists, its marker, and its Go files too, since go list leaves a
|
|
||||||
// file its build tags exclude (an integration test a unit test globs) out of what the digest hashes. A file the
|
|
||||||
// toolchain lists is hashed twice, which changes nothing.
|
|
||||||
err = walkFiles(root.Dir, add)
|
|
||||||
return lines, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanTreePattern says a marker's line is a path from the repository's root that stays inside it.
|
|
||||||
func cleanTreePattern(p string) bool {
|
|
||||||
if p == "" || strings.ContainsAny(p, " \t\\") || strings.HasPrefix(p, "/") || path.Clean(p) != p {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, e := range strings.Split(p, "/") {
|
|
||||||
if e == ".." || e == "." {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// walkFiles calls each with every file at or under path, skipping .git and node_modules, in lexical order.
|
|
||||||
func walkFiles(path string, each func(string) error) error {
|
|
||||||
return filepath.WalkDir(path, func(p string, e fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.IsDir() {
|
|
||||||
if n := e.Name(); p != path && (n == ".git" || n == "node_modules") {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return each(p)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) {
|
|||||||
testMain := &goPackage{ImportPath: "x/r.test", Deps: []string{ext.ImportPath}}
|
testMain := &goPackage{ImportPath: "x/r.test", Deps: []string{ext.ImportPath}}
|
||||||
pkgs[root.ImportPath] = root
|
pkgs[root.ImportPath] = root
|
||||||
digest := func() string {
|
digest := func() string {
|
||||||
d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}, nil, nil)
|
d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -190,74 +190,28 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue 1935, 1936: a package whose reads-outside marker lists nothing it can trust is never kept: its digest is one no
|
// Issue 1935: a package whose directory carries the reads-outside marker is never kept: its digest is one no kept verdict
|
||||||
// kept verdict can have (it differs on every call, though nothing changed), and the answer names it. A marker that lists
|
// can have (it differs on every call, though nothing changed), and the answer names it. A package without the marker
|
||||||
// what the tests read is kept: a change to a file it names changes the package's digest, a change elsewhere does not.
|
// keeps its digest.
|
||||||
func TestAPackageWithTheReadsOutsideMarkerIsKeptOnlyByWhatItLists(t *testing.T) {
|
func TestAPackageWithTheReadsOutsideMarkerIsNeverKept(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
m, tree := digestFixture(t)
|
m, tree := digestFixture(t)
|
||||||
plain := digests(t, m, "unit")
|
plain := digests(t, m, "unit")
|
||||||
if len(plain.ReadsOutside) != 0 {
|
if len(plain.ReadsOutside) != 0 {
|
||||||
t.Fatalf("a package carries the marker in the fixture: %v", plain.ReadsOutside)
|
t.Fatalf("a package carries the marker in the fixture: %v", plain.ReadsOutside)
|
||||||
}
|
}
|
||||||
c, a := "fixture/digest/c", "fixture/digest/a"
|
edit(t, tree, "c/"+ReadsOutsideMarker, "the tests read ../docs\n")
|
||||||
if err := os.MkdirAll(filepath.Join(tree, "docs"), 0o755); err != nil {
|
first, second := digests(t, m, "unit"), digests(t, m, "unit")
|
||||||
t.Fatal(err)
|
c := "fixture/digest/c"
|
||||||
|
if len(first.Packages[c]) != 64 || first.Packages[c] == second.Packages[c] || first.Packages[c] == plain.Packages[c] {
|
||||||
|
t.Errorf("c carries the marker and its digest is stable: %s, %s, before %s", first.Packages[c], second.Packages[c], plain.Packages[c])
|
||||||
}
|
}
|
||||||
edit(t, tree, "docs/one.md", "one\n")
|
if strings.Join(first.ReadsOutside, ",") != c {
|
||||||
edit(t, tree, "other.md", "other\n")
|
t.Errorf("the answer does not name the package that reads outside: %v", first.ReadsOutside)
|
||||||
for name, marker := range map[string]string{
|
|
||||||
"an empty marker": "# nothing listed\n\n",
|
|
||||||
"a sentence": "the tests read ../docs\n",
|
|
||||||
"a path climbing out": "../docs\n",
|
|
||||||
"a path matching nothing": "docs/none.md\n",
|
|
||||||
"an absolute path": "/etc/passwd\n",
|
|
||||||
"a path that is not clean": "docs/./one.md\n",
|
|
||||||
"a good line and a bad line": "docs\nnope/*.md\n",
|
|
||||||
} {
|
|
||||||
edit(t, tree, "c/"+ReadsOutsideMarker, marker)
|
|
||||||
first, second := digests(t, m, "unit"), digests(t, m, "unit")
|
|
||||||
if len(first.Packages[c]) != 64 || first.Packages[c] == second.Packages[c] || first.Packages[c] == plain.Packages[c] {
|
|
||||||
t.Errorf("%s: c's digest is stable: %s, %s, before %s", name, first.Packages[c], second.Packages[c], plain.Packages[c])
|
|
||||||
}
|
|
||||||
if strings.Join(first.ReadsOutside, ",") != c {
|
|
||||||
t.Errorf("%s: the answer does not name the package that reads outside: %v", name, first.ReadsOutside)
|
|
||||||
}
|
|
||||||
if first.Packages[a] != plain.Packages[a] || first.Packages[a] != second.Packages[a] {
|
|
||||||
t.Errorf("%s: a has no marker and its digest moved: %s, %s, before %s", name, first.Packages[a], second.Packages[a], plain.Packages[a])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
a := "fixture/digest/a"
|
||||||
// A list: a glob, a directory read whole, and a path whose absence is read.
|
if first.Packages[a] != plain.Packages[a] || first.Packages[a] != second.Packages[a] {
|
||||||
edit(t, tree, "c/"+ReadsOutsideMarker, "# what c's tests read\ndocs/*.md\ndocs\n?gone\n")
|
t.Errorf("a has no marker and its digest moved: %s, %s, before %s", first.Packages[a], second.Packages[a], plain.Packages[a])
|
||||||
before := digests(t, m, "unit")
|
|
||||||
if len(before.ReadsOutside) != 0 || len(before.Packages[c]) != 64 || before.Packages[c] == plain.Packages[c] {
|
|
||||||
t.Fatalf("a listed marker is not kept: %v, %s (no marker %s)", before.ReadsOutside, before.Packages[c], plain.Packages[c])
|
|
||||||
}
|
|
||||||
if again := digests(t, m, "unit"); again.Packages[c] != before.Packages[c] {
|
|
||||||
t.Errorf("nothing changed and c's digest moved")
|
|
||||||
}
|
|
||||||
for _, step := range []struct {
|
|
||||||
file, text string
|
|
||||||
moves bool
|
|
||||||
}{
|
|
||||||
{"other.md", "changed\n", false}, // outside the list
|
|
||||||
{"d/d.go", "package d\n\n// D.\n", false}, // a package c's tests do not reach
|
|
||||||
{"docs/one.md", "changed\n", true}, // a listed file
|
|
||||||
{"docs/two.md", "new\n", true}, // a new file the glob matches
|
|
||||||
{"gone", "here\n", true}, // a path whose absence was read
|
|
||||||
{"c/note.txt", "changed\n", true}, // the package's own file that is not Go
|
|
||||||
{"c/x_integration_test.go", "//go:build integration\n\npackage c\n", true}, // a Go file its build tags leave out (go list's IgnoredGoFiles)
|
|
||||||
} {
|
|
||||||
edit(t, tree, step.file, step.text)
|
|
||||||
after := digests(t, m, "unit")
|
|
||||||
if moved := after.Packages[c] != before.Packages[c]; moved != step.moves {
|
|
||||||
t.Errorf("a change to %s: c's digest moved %v, want %v", step.file, moved, step.moves)
|
|
||||||
}
|
|
||||||
if after.Packages[a] != plain.Packages[a] {
|
|
||||||
t.Errorf("a change to %s moved a's digest", step.file)
|
|
||||||
}
|
|
||||||
before = after
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue 890/AC3: a package whose directory carries the needs marker is named with its needs at the integration level,
|
// Issue 890/AC3: a package whose directory carries the needs marker is named with its needs at the integration level,
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A failed test keeps the lines that say why it failed, not the lines it logged first (issue 1901). The fixture is
|
|
||||||
// the real `go test -json` of a test that logs a line and then fails with a multi-line t.Fatalf.
|
|
||||||
func TestAFailedTestKeepsItsFatalMessageAfterItsLogLines(t *testing.T) {
|
|
||||||
raw, err := os.ReadFile("testdata/captured/go-test-json-log-then-fatal.jsonl")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
res := &TestResult{}
|
|
||||||
rd := newReader(res, ".")
|
|
||||||
rd.read(string(raw))
|
|
||||||
rd.finish()
|
|
||||||
if len(res.Failed) != 1 {
|
|
||||||
t.Fatalf("failures = %+v, want one", res.Failed)
|
|
||||||
}
|
|
||||||
f := res.Failed[0]
|
|
||||||
for _, want := range []string{"3 subscription(s) derive to a subject no emitter owns:", "a.b.c", "d.e.f", "g.h.i"} {
|
|
||||||
if !strings.Contains(f.Output, want) {
|
|
||||||
t.Errorf("1901/AC1: output lacks %q: %q", want, f.Output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if want := "3 subscription(s) derive to a subject no emitter owns:"; f.Cause != want && !strings.HasSuffix(f.Cause, want) {
|
|
||||||
t.Errorf("1901/AC1: cause = %q, want the t.Fatalf line, not the t.Logf line", f.Cause)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A failure longer than the bound keeps its last lines, where the cause is, and says how many earlier lines it dropped.
|
|
||||||
func TestAFailureLongerThanTheBoundKeepsItsLastLines(t *testing.T) {
|
|
||||||
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)")
|
|
||||||
out := clip(lines, "")
|
|
||||||
if len(out) > outputBytes+100 {
|
|
||||||
t.Errorf("1901/AC1: kept %d bytes, bound is %d", len(out), outputBytes)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(out, "the real cause\n--- FAIL: TestX (0.00s)") || !strings.Contains(out, "\n... (") {
|
|
||||||
t.Errorf("1901/AC1: out = %q...%q", out[:40], out[len(out)-60:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// An older checker, which knows no "cause" field, still reads the new build module's answer, and the output it gets
|
|
||||||
// holds at least what the older build module kept (its first lines, for an output shorter than the bound).
|
|
||||||
func TestAnOlderCheckerReadsTheNewFailureAndGetsAtLeastWhatItKept(t *testing.T) { // 1901/AC3
|
|
||||||
raw, err := os.ReadFile("testdata/captured/go-test-json-log-then-fatal.jsonl")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
res := &TestResult{}
|
|
||||||
rd := newReader(res, ".")
|
|
||||||
rd.read(string(raw))
|
|
||||||
rd.finish()
|
|
||||||
b, err := json.Marshal(res.Failed)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var old []struct {
|
|
||||||
Package string `json:"package"`
|
|
||||||
Test string `json:"test,omitempty"`
|
|
||||||
Output string `json:"output,omitempty"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(b, &old); err != nil || len(old) != 1 {
|
|
||||||
t.Fatalf("1901/AC3: an older checker refused the answer: %v, %d failures", err, len(old))
|
|
||||||
}
|
|
||||||
first := strings.SplitN(old[0].Output, "\n", 2)[0]
|
|
||||||
if first == "" || !strings.Contains(old[0].Output, "judged against") || !strings.Contains(old[0].Output, "3 subscription(s)") {
|
|
||||||
t.Errorf("1901/AC3: older checker's output = %q", old[0].Output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A long failure, whose earlier lines are left out, still starts with a real line for an older checker, which keeps only the
|
|
||||||
// first non-empty line of the output cut to 120 characters, and not with the marker.
|
|
||||||
func TestALongFailureStillStartsWithARealLineForAnOlderChecker(t *testing.T) { // 1901/AC3
|
|
||||||
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)
|
|
||||||
for _, l := range strings.Split(out, "\n") {
|
|
||||||
if l = strings.TrimSpace(l); l != "" {
|
|
||||||
if len(l) > 120 {
|
|
||||||
l = l[:120]
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
for _, cause := range []string{"the real cause", ""} {
|
|
||||||
out := clip(lines, cause)
|
|
||||||
got := oldFirst(out)
|
|
||||||
if strings.HasPrefix(got, "... (") || got == "" {
|
|
||||||
t.Errorf("1901/AC3: cause %q: an older checker would read %q", cause, got)
|
|
||||||
}
|
|
||||||
if cause != "" && got != cause {
|
|
||||||
t.Errorf("1901/AC3: older checker reads %q, want the cause", got)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(out, "--- FAIL: TestX (0.00s)") {
|
|
||||||
t.Errorf("1901/AC3: the last lines are lost: %q", out[len(out)-40:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,13 +42,11 @@ type FetchResult struct {
|
|||||||
// head onto it, the paths the merge changes, and the tree of each module directory (a directory holding a
|
// head onto it, the paths the merge changes, and the tree of each module directory (a directory holding a
|
||||||
// module.json) in the merge. A head that does not merge cleanly is an answer: Merge is empty, Conflicts names the
|
// module.json) in the merge. A head that does not merge cleanly is an answer: Merge is empty, Conflicts names the
|
||||||
// files, and the workspace is left at the trunk commit.
|
// files, and the workspace is left at the trunk commit.
|
||||||
Base string `json:"base,omitempty"`
|
Base string `json:"base,omitempty"`
|
||||||
Merge string `json:"merge,omitempty"`
|
Merge string `json:"merge,omitempty"`
|
||||||
Conflicts []string `json:"conflicts,omitempty"`
|
Conflicts []string `json:"conflicts,omitempty"`
|
||||||
Changed []string `json:"changed,omitempty"`
|
Changed []string `json:"changed,omitempty"`
|
||||||
// Programs are the changed files that are compiled programs (issue 1577); a merge answers it, a pointer to an empty list when there are none, absent for a fetch that made no merge.
|
Trees map[string]buildstep.ModuleTree `json:"trees,omitempty"`
|
||||||
Programs *[]buildstep.Program `json:"programs,omitempty"`
|
|
||||||
Trees map[string]buildstep.ModuleTree `json:"trees,omitempty"`
|
|
||||||
// Beside are the sibling repositories placed as plain directories under the workspace's beside directory, which
|
// Beside are the sibling repositories placed as plain directories under the workspace's beside directory, which
|
||||||
// build_go_test hands to the tests as MESH_CHECK_BESIDE (ADR 0484 clause 4).
|
// build_go_test hands to the tests as MESH_CHECK_BESIDE (ADR 0484 clause 4).
|
||||||
Beside []Sibling `json:"beside,omitempty"`
|
Beside []Sibling `json:"beside,omitempty"`
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1901/AC7: the module's own parse path (readGoTest, then the level's judging) keeps the failure contract on every captured
|
||||||
|
// go test -json case of the SDK's conformance kit, and its result names the module that sent it.
|
||||||
|
func TestTheTestStepKeepsTheFailureContract(t *testing.T) {
|
||||||
|
conformance.Run(t, conformance.GoTest, func(o conformance.Output) []buildstep.Failure {
|
||||||
|
res := &TestResult{Level: LevelUnit, By: buildstep.By("mesh-build-go")}
|
||||||
|
readGoTest(res, ".", o.Stdout)
|
||||||
|
res.judge(Bounds{Run: UnitBound})
|
||||||
|
if res.Passed {
|
||||||
|
t.Errorf("a failed run judged passed: %+v", res)
|
||||||
|
}
|
||||||
|
return res.Failed
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,14 +6,12 @@ package main
|
|||||||
// step that could not run.
|
// step that could not run.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The two levels (ADR 0445 §2).
|
// The two levels (ADR 0445 §2).
|
||||||
@@ -32,39 +30,14 @@ type Bounds struct {
|
|||||||
Setup, Run time.Duration
|
Setup, Run time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageResult is one package's part of a level.
|
// PackageResult, TestTime and Failure are the SDK's failure contract (buildstep, issue 1901), with the JSON names this
|
||||||
type PackageResult struct {
|
// module always sent: a Failure names its test, its file, its cause (never empty, never a passing line) and its own
|
||||||
Dir string `json:"dir"`
|
// output up to 4096 bytes, cause first.
|
||||||
Package string `json:"package"`
|
type (
|
||||||
Passed bool `json:"passed"`
|
PackageResult = buildstep.GoPackage
|
||||||
Elapsed float64 `json:"elapsed"`
|
TestTime = buildstep.TestTime
|
||||||
Tests int `json:"tests"`
|
Failure = buildstep.Failure
|
||||||
// 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"`
|
|
||||||
// Skipped is how many of the package's tests skipped: a pass that skipped proved less than it says, so the checker keeps no verdict for it.
|
|
||||||
Skipped int `json:"skipped,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTime is one test and the time it took.
|
|
||||||
type TestTime struct {
|
|
||||||
Package string `json:"package"`
|
|
||||||
Test string `json:"test"`
|
|
||||||
Elapsed float64 `json:"elapsed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failure is a test that failed, or a package that failed with no test to name (a build that failed, a TestMain that
|
|
||||||
// panicked); Output is the last lines it wrote, up to outputBytes (issue 1901), and Cause the first line of what the
|
|
||||||
// test reported as its error (t.Error, t.Fatal), without its file and line, when go test said which lines those were.
|
|
||||||
// **A failure that is not one test's assertion carries its real cause too** (issue 1901, its third meeting): the
|
|
||||||
// "panic:" line (a timeout's with the tests it found running), a "fatal error:" or "signal:" line, a build's first
|
|
||||||
// error, or in words that the test program exited, while which tests ran or after its tests passed. Test then names the
|
|
||||||
// tests that were running when the program stopped. A "PASS" or "ok" line is never a failure's words.
|
|
||||||
type Failure struct {
|
|
||||||
Package string `json:"package"`
|
|
||||||
Test string `json:"test,omitempty"`
|
|
||||||
Output string `json:"output,omitempty"`
|
|
||||||
Cause string `json:"cause,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestResult is what a test step passes with.
|
// TestResult is what a test step passes with.
|
||||||
type TestResult struct {
|
type TestResult struct {
|
||||||
@@ -98,368 +71,23 @@ type TestResult struct {
|
|||||||
// JudgedBy is the program that ran the level (ADR 0535 §5): "installed", or "candidate <merge commit>" under the
|
// JudgedBy is the program that ran the level (ADR 0535 §5): "installed", or "candidate <merge commit>" under the
|
||||||
// runner workspace.
|
// runner workspace.
|
||||||
JudgedBy string `json:"judged_by,omitempty"`
|
JudgedBy string `json:"judged_by,omitempty"`
|
||||||
|
// By is the build module and the SDK it was built with (buildstep.By): the checker names it when a failure hides
|
||||||
|
// its cause.
|
||||||
|
By string `json:"by,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// event is one line of `go test -json` (test2json, and the build events go test adds).
|
// readGoTest reads one go.mod directory's `go test -json` into res through the SDK's reader (issue 1901: one failure
|
||||||
type event struct {
|
// contract for every build module, buildstep.Failure), and answers the lines that were no event and how many packages
|
||||||
Action string
|
// it named.
|
||||||
Package string
|
func readGoTest(res *TestResult, dir, out string) (noise []string, packages int) {
|
||||||
Test string
|
read := buildstep.ReadGoTest(out, dir)
|
||||||
Output string
|
res.Tests += read.Tests
|
||||||
OutputType string // "error" for a t.Error or t.Fatal message, "error-continue" for its further lines (go test -json)
|
res.Skipped += read.Skipped
|
||||||
ImportPath string
|
res.Failed = append(res.Failed, read.Failed...)
|
||||||
FailedBuild string
|
res.Packages = append(res.Packages, read.Packages...)
|
||||||
Elapsed float64
|
res.Slowest = append(res.Slowest, read.Times...)
|
||||||
}
|
res.Elapsed = max(res.Elapsed, read.Elapsed)
|
||||||
|
return read.Noise, len(read.Packages)
|
||||||
// outputBytes is the most a failure keeps of its output, and outputLineBytes the most of one line. **A failure keeps its
|
|
||||||
// last lines** (issue 1901): a test logs before it fails, so the lines that say why are at the end, and the first
|
|
||||||
// fifteen lines kept before lost them (the verdict of group r4g kept a t.Logf line, not the t.Fatalf after it). Four
|
|
||||||
// kilobytes holds a long assertion message and stays far under a bus message's or a verdict's bound.
|
|
||||||
const (
|
|
||||||
outputBytes = 4096
|
|
||||||
outputLineBytes = 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
// reader collects the events of one go.mod directory's `go test -json` into a result.
|
|
||||||
type reader struct {
|
|
||||||
res *TestResult
|
|
||||||
dir string
|
|
||||||
pkgs map[string]*PackageResult
|
|
||||||
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
|
|
||||||
times []TestTime
|
|
||||||
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 (issue 1901).
|
|
||||||
running map[string][]string
|
|
||||||
// passed says a package's test program printed "PASS" (its tests passed) before it failed; built says its test
|
|
||||||
// program failed to build.
|
|
||||||
passed, built map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newReader(res *TestResult, dir string) *reader {
|
|
||||||
return &reader{res: res, dir: dir, pkgs: map[string]*PackageResult{}, 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{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// read takes go test's output. Lines that are not events are kept for a failure that has nothing else to say.
|
|
||||||
func (r *reader) read(out string) (noise []string) {
|
|
||||||
sc := bufio.NewScanner(strings.NewReader(out))
|
|
||||||
sc.Buffer(make([]byte, 1<<20), 1<<26)
|
|
||||||
for sc.Scan() {
|
|
||||||
var ev event
|
|
||||||
if err := json.Unmarshal(sc.Bytes(), &ev); err != nil {
|
|
||||||
if l := strings.TrimSpace(sc.Text()); l != "" {
|
|
||||||
noise = append(noise, l)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r.event(ev)
|
|
||||||
}
|
|
||||||
return noise
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *reader) pkg(name string) *PackageResult {
|
|
||||||
p := r.pkgs[name]
|
|
||||||
if p == nil {
|
|
||||||
p = &PackageResult{Dir: r.dir, Package: name}
|
|
||||||
r.pkgs[name] = p
|
|
||||||
r.order = append(r.order, name)
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func keep(lines []string, out string) []string {
|
|
||||||
if t := strings.TrimRight(out, "\r\n"); t != "" && !strings.HasPrefix(t, "=== ") {
|
|
||||||
lines = append(lines, t)
|
|
||||||
}
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *reader) event(ev event) {
|
|
||||||
switch {
|
|
||||||
case ev.Package == "" && ev.ImportPath != "":
|
|
||||||
if ev.Action == "build-output" {
|
|
||||||
r.builds[ev.ImportPath] = keep(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 ("PASS", "FAIL", "ok <pkg>", "FAIL <pkg>") is never a failure's
|
|
||||||
// words: a "PASS" before a "FAIL <pkg>" 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 summaryLine(l):
|
|
||||||
default:
|
|
||||||
r.pkgOut[ev.Package] = keep(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] = keep(r.testOut[key], ev.Output)
|
|
||||||
if ev.OutputType == "error" && r.causes[key] == "" {
|
|
||||||
r.causes[key] = errorWords(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.times = append(r.times, TestTime{Package: ev.Package, Test: ev.Test, Elapsed: ev.Elapsed})
|
|
||||||
if ev.Action == "fail" {
|
|
||||||
r.failedIn[ev.Package] = true
|
|
||||||
cause := r.causes[key]
|
|
||||||
if race := raceCause(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 = causeOf(r.testOut[key])
|
|
||||||
}
|
|
||||||
r.res.Failed = append(r.res.Failed, Failure{Package: ev.Package, Test: ev.Test, Output: clip(r.testOut[key], cause), Cause: cause})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clip joins the last lines of a failure's output that fit outputBytes, each cut to outputLineBytes, and says how many
|
|
||||||
// earlier lines it left out. **Its first line is the failure's cause** (issue 1901/AC3, AC4), put ahead when the kept
|
|
||||||
// lines do not start with it, or, with no cause and lines left out, the first non-empty line of the output, then the
|
|
||||||
// marker. An older checker takes only the first non-empty line of the output, and a marker there would be less than the
|
|
||||||
// older pair kept.
|
|
||||||
func clip(lines []string, cause string) string {
|
|
||||||
size, from := 0, len(lines)
|
|
||||||
for from > 0 {
|
|
||||||
n := len(cutLine(lines[from-1])) + 1
|
|
||||||
if size+n > outputBytes && from < len(lines) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
size += n
|
|
||||||
from--
|
|
||||||
}
|
|
||||||
kept := make([]string, 0, len(lines)-from+2)
|
|
||||||
head := strings.TrimSpace(cause)
|
|
||||||
for i := 0; head == "" && i < from; i++ {
|
|
||||||
head = strings.TrimSpace(lines[i])
|
|
||||||
}
|
|
||||||
if head != "" && (from > 0 || len(lines) == 0 || strings.TrimSpace(lines[from]) != head) {
|
|
||||||
kept = append(kept, cutLine(head))
|
|
||||||
}
|
|
||||||
if from > 0 {
|
|
||||||
kept = append(kept, fmt.Sprintf("... (%d earlier lines)", from))
|
|
||||||
}
|
|
||||||
for _, l := range lines[from:] {
|
|
||||||
kept = append(kept, cutLine(l))
|
|
||||||
}
|
|
||||||
return strings.Join(kept, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// summaryLine says a line is go test's summary of a package, in go's exact forms only: "PASS" or "FAIL" alone,
|
|
||||||
// "ok \t<pkg>\t<time>" and "FAIL\t<pkg>\t<time>" (review round 1: a line of the package's own that starts "ok " is kept).
|
|
||||||
func summaryLine(l string) bool {
|
|
||||||
return l == "PASS" || l == "FAIL" || strings.HasPrefix(l, "ok \t") || strings.HasPrefix(l, "FAIL\t")
|
|
||||||
}
|
|
||||||
|
|
||||||
// causeOf is the cause in a failure's output that is not a test's assertion (issue 1901): a race the race detector
|
|
||||||
// reported (raceCause); else the first "panic:" line, a timeout's with the tests it names as running; else the first
|
|
||||||
// "fatal error:" (the runtime's) or "signal:" line (a test program killed); "" when there is none. It reads every line,
|
|
||||||
// so a long goroutine dump after the panic cannot hide it. **It matches only lines at column 0**, where go prints the
|
|
||||||
// runtime's: a test's indented log line holding the same words is not the cause (review round 1).
|
|
||||||
func causeOf(lines []string) string {
|
|
||||||
if race := raceCause(lines); race != "" {
|
|
||||||
return race
|
|
||||||
}
|
|
||||||
for _, prefix := range []string{"panic: ", "fatal error: ", "signal: "} {
|
|
||||||
for i, l := range lines {
|
|
||||||
l = strings.TrimRight(l, "\r\n") // at column 0: an indented line is a test's own log, not the runtime's
|
|
||||||
if !strings.HasPrefix(l, prefix) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(l, "panic: test timed out after") {
|
|
||||||
var names []string
|
|
||||||
for _, n := range lines[i+1:] {
|
|
||||||
if !strings.HasPrefix(n, "\t\t") {
|
|
||||||
if strings.TrimSpace(n) == "running tests:" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
names = append(names, strings.TrimSpace(n))
|
|
||||||
}
|
|
||||||
if len(names) > 0 {
|
|
||||||
l += "; running tests: " + strings.Join(names, ", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cutLine(l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// raceCause is the first race the race detector reported in a failure's output, in one line (issue 1901: a race after
|
|
||||||
// the tests' "PASS" was worded "PASS"): its access and the frame that made it, and where the goroutine that made it was
|
|
||||||
// created, as "WARNING: DATA RACE: Write at 0x... by goroutine 10 in pkg.F.func1() (f_test.go:13); goroutine created at
|
|
||||||
// pkg.F() (f_test.go:13)". "" when the output holds no race.
|
|
||||||
func raceCause(lines []string) string {
|
|
||||||
at := -1
|
|
||||||
for i, l := range lines {
|
|
||||||
if strings.TrimRight(l, "\r\n") == "WARNING: DATA RACE" { // at column 0, as causeOf
|
|
||||||
at = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if at < 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
// frame is the function and the file's base name and line that follow a heading line at i.
|
|
||||||
frame := func(i int) string {
|
|
||||||
var fn, file string
|
|
||||||
for _, l := range lines[i+1:] {
|
|
||||||
if l = strings.TrimSpace(l); l == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if fn == "" {
|
|
||||||
fn = l
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
file = l
|
|
||||||
if j := strings.LastIndexByte(file, '/'); j >= 0 {
|
|
||||||
file = file[j+1:]
|
|
||||||
}
|
|
||||||
if j := strings.Index(file, " +0x"); j >= 0 {
|
|
||||||
file = file[:j]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if file != "" {
|
|
||||||
return fn + " (" + file + ")"
|
|
||||||
}
|
|
||||||
return fn
|
|
||||||
}
|
|
||||||
words := "WARNING: DATA RACE"
|
|
||||||
for i := at + 1; i < len(lines); i++ {
|
|
||||||
l := strings.TrimSpace(lines[i])
|
|
||||||
if l == "" || strings.HasPrefix(l, "===") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
words += ": " + strings.TrimSuffix(l, ":")
|
|
||||||
if f := frame(i); f != "" {
|
|
||||||
words += " in " + f
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for i := at + 1; i < len(lines); i++ {
|
|
||||||
l := strings.TrimSpace(lines[i])
|
|
||||||
if strings.HasPrefix(l, "==================") {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(l, "Goroutine ") && strings.HasSuffix(l, " created at:") {
|
|
||||||
if f := frame(i); f != "" {
|
|
||||||
words += "; its goroutine created at " + f
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cutLine(words)
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildCause is the first error line of a failed build: the first line that is not go's "# <package>" heading.
|
|
||||||
func buildCause(lines []string) string {
|
|
||||||
for _, l := range lines {
|
|
||||||
if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "# ") {
|
|
||||||
return cutLine(l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// testNames names the tests that were running, at most three, and how many more.
|
|
||||||
func testNames(tests []string) string {
|
|
||||||
if len(tests) <= 3 {
|
|
||||||
return strings.Join(tests, ", ")
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s and %d more", strings.Join(tests[:3], ", "), len(tests)-3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// packageFailure is a package that failed with no failed test to name it (issue 1901): its test program did not build,
|
|
||||||
// panicked, timed out, was killed or exited. It names the tests that were running when the program stopped, keeps
|
|
||||||
// their output with the package's, and says the cause.
|
|
||||||
func (r *reader) packageFailure(name string) Failure {
|
|
||||||
running := r.running[name]
|
|
||||||
var lines []string
|
|
||||||
for _, t := range running {
|
|
||||||
lines = append(lines, r.testOut[name+"\x00"+t]...)
|
|
||||||
}
|
|
||||||
lines = append(lines, r.pkgOut[name]...)
|
|
||||||
cause := causeOf(lines)
|
|
||||||
switch {
|
|
||||||
case cause != "":
|
|
||||||
case r.built[name]:
|
|
||||||
cause = buildCause(lines)
|
|
||||||
case len(running) > 0:
|
|
||||||
cause = "the test program exited while " + testNames(running) + " ran, with no panic: an os.Exit or a signal (go test -json reports no exit status)"
|
|
||||||
case r.passed[name]:
|
|
||||||
cause = "the tests passed, then the test program exited with a failure: an os.Exit or a panic in TestMain or after the tests (go test -json reports no exit status)"
|
|
||||||
}
|
|
||||||
if cause == "" {
|
|
||||||
cause = "the package failed with no test to name and no panic, signal or build error in its output"
|
|
||||||
}
|
|
||||||
return Failure{Package: name, Test: testNames(running), Output: clip(lines, cause), Cause: cause}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cutLine cuts a line to outputLineBytes, on a character boundary.
|
|
||||||
func cutLine(l string) string {
|
|
||||||
if len(l) <= outputLineBytes {
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
return strings.ToValidUTF8(l[:outputLineBytes], "") + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileAndLine = regexp.MustCompile(`^\S+\.go:\d+: `)
|
|
||||||
|
|
||||||
// errorWords is the words of an error line of a test: trimmed, without the file and line go test put before them.
|
|
||||||
func errorWords(out string) string {
|
|
||||||
return strings.TrimSpace(fileAndLine.ReplaceAllString(strings.TrimSpace(out), ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
// finish moves what was read into the result: the packages in order, a failure for each failed package that has no
|
|
||||||
// failed test to name it, and the tests' times.
|
|
||||||
func (r *reader) finish() {
|
|
||||||
for _, name := range r.order {
|
|
||||||
p := r.pkgs[name]
|
|
||||||
if !p.Passed && !r.failedIn[name] {
|
|
||||||
r.res.Failed = append(r.res.Failed, r.packageFailure(name))
|
|
||||||
}
|
|
||||||
if !p.NoTests && p.Elapsed > r.res.Elapsed {
|
|
||||||
r.res.Elapsed = p.Elapsed
|
|
||||||
}
|
|
||||||
r.res.Packages = append(r.res.Packages, *p)
|
|
||||||
}
|
|
||||||
r.res.Slowest = append(r.res.Slowest, r.times...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// judge sets Passed and Why from what was read and the bounds. A level with no failure, over no bound, passes; with no
|
// judge sets Passed and Why from what was read and the bounds. A level with no failure, over no bound, passes; with no
|
||||||
|
|||||||
@@ -104,8 +104,7 @@ func (m *Module) lint(ctx context.Context, run forgekey.Runner, tree, d string,
|
|||||||
}
|
}
|
||||||
var failed []Failure
|
var failed []Failure
|
||||||
if len(files) > 0 {
|
if len(files) > 0 {
|
||||||
failed = append(failed, Failure{Package: d, Test: "gofmt",
|
failed = append(failed, buildstep.ConsoleFailure(d, "gofmt", "not as gofmt writes them (run gofmt -w): "+strings.Join(files, ", ")))
|
||||||
Output: "not as gofmt writes them (run gofmt -w): " + strings.Join(files, ", ")})
|
|
||||||
}
|
}
|
||||||
sets := [][]string{nil}
|
sets := [][]string{nil}
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
@@ -124,7 +123,7 @@ func (m *Module) lint(ctx context.Context, run forgekey.Runner, tree, d string,
|
|||||||
return nil, fmt.Errorf("%s in %s could not be run: %s", name, d, firstLine(r.Err+"\n"+r.Stderr))
|
return nil, fmt.Errorf("%s in %s could not be run: %s", name, d, firstLine(r.Err+"\n"+r.Stderr))
|
||||||
}
|
}
|
||||||
if r.Status != 0 {
|
if r.Status != 0 {
|
||||||
failed = append(failed, Failure{Package: d, Test: name, Output: strings.TrimSpace(r.Stderr + "\n" + r.Stdout)})
|
failed = append(failed, buildstep.ConsoleFailure(d, name, r.Stderr+"\n"+r.Stdout))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return failed, nil
|
return failed, nil
|
||||||
@@ -133,9 +132,6 @@ func (m *Module) lint(ctx context.Context, run forgekey.Runner, tree, d string,
|
|||||||
// lintAll lints each directory, up to m.parallel() at once like the level's tests; the failures join in directory order
|
// lintAll lints each directory, up to m.parallel() at once like the level's tests; the failures join in directory order
|
||||||
// and the error is the first directory's, so the answer does not depend on which ended first (issue 1930).
|
// and the error is the first directory's, so the answer does not depend on which ended first (issue 1930).
|
||||||
func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string, dirs []string, env func(d string) []string) ([]Failure, error) {
|
func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string, dirs []string, env func(d string) []string) ([]Failure, error) {
|
||||||
// The ask's phases say it (issue 1890): lint of the directories, then their tests.
|
|
||||||
buildstep.Phase(ctx, fmt.Sprintf("unit: lint of %d directories", len(dirs)))
|
|
||||||
defer buildstep.Phase(ctx, fmt.Sprintf("unit: tests of %d directories", len(dirs)))
|
|
||||||
type answer struct {
|
type answer struct {
|
||||||
failed []Failure
|
failed []Failure
|
||||||
err error
|
err error
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A failure that is not one test's assertion carries its real cause (issue 1901, its third meeting: a try of
|
|
||||||
// mesh-controller kept only "PASS" for a package that stopped half-way). Each fixture is the real `go test -json` of a
|
|
||||||
// throwaway package (testdata/captured/README).
|
|
||||||
func TestAFailureThatIsNotAnAssertionCarriesItsRealCause(t *testing.T) { // 1901/AC4
|
|
||||||
cases := []struct {
|
|
||||||
fixture string
|
|
||||||
test string // the test the failure names; "" for none
|
|
||||||
cause []string // what the cause holds
|
|
||||||
}{
|
|
||||||
{"panicking", "TestPanics", []string{"panic: assignment to entry in nil map"}},
|
|
||||||
{"timeout", "TestWaitsForever", []string{"panic: test timed out after 2s", "TestWaitsForever"}},
|
|
||||||
{"deeptimeout", "TestHoldsTheSeat, TestWaitsForTheBus", []string{"panic: test timed out after 2s", "TestHoldsTheSeat", "TestWaitsForTheBus"}},
|
|
||||||
{"buildfail", "", []string{`cannot use "seat" (untyped string constant) as int value`}},
|
|
||||||
{"exiting", "TestExits", []string{"exited", "TestExits"}},
|
|
||||||
{"killed", "TestKilled", []string{"signal: killed"}},
|
|
||||||
{"passthenfail", "", []string{"panic: the bus closed after the tests: nats: connection closed"}},
|
|
||||||
{"mainexit", "", []string{"passed", "exited"}},
|
|
||||||
{"race", "TestRacesOnTheSeat", []string{"DATA RACE", "Write at", "example.com/race.TestRacesOnTheSeat.func1()", "p_test.go:13", "created at example.com/race.TestRacesOnTheSeat()"}},
|
|
||||||
{"racemain", "", []string{"DATA RACE", "Write at", "example.com/racemain.TestMain.func1()", "p_test.go:15", "created at example.com/racemain.TestMain()"}},
|
|
||||||
}
|
|
||||||
for _, c := range cases {
|
|
||||||
t.Run(c.fixture, func(t *testing.T) {
|
|
||||||
raw, err := os.ReadFile("testdata/captured/go-test-json-pkg-" + c.fixture + ".jsonl")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
res := &TestResult{}
|
|
||||||
rd := newReader(res, ".")
|
|
||||||
rd.read(string(raw))
|
|
||||||
rd.finish()
|
|
||||||
if len(res.Failed) != 1 {
|
|
||||||
t.Fatalf("failures = %+v, want one", res.Failed)
|
|
||||||
}
|
|
||||||
f := res.Failed[0]
|
|
||||||
if f.Test != c.test {
|
|
||||||
t.Errorf("1901/AC4: the failure names test %q, want %q", f.Test, c.test)
|
|
||||||
}
|
|
||||||
for _, want := range c.cause {
|
|
||||||
if !strings.Contains(f.Cause, want) {
|
|
||||||
t.Errorf("1901/AC4: cause = %q, want it to hold %q", f.Cause, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(f.Cause) > outputLineBytes+3 {
|
|
||||||
t.Errorf("1901/AC4: cause holds %d bytes, over the line bound %d", len(f.Cause), outputLineBytes)
|
|
||||||
}
|
|
||||||
// Never a "PASS" or "ok" line as a failure's words, and the first line of the output, all an older
|
|
||||||
// checker reads, is the cause.
|
|
||||||
for _, l := range strings.Split(f.Output, "\n") {
|
|
||||||
if l = strings.TrimSpace(l); l == "PASS" || strings.HasPrefix(l, "ok ") || strings.HasPrefix(l, "ok\t") {
|
|
||||||
t.Errorf("1901/AC4: the output keeps the line %q: %q", l, f.Output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if first := strings.SplitN(f.Output, "\n", 2)[0]; strings.TrimSpace(first) != strings.TrimSpace(f.Cause) {
|
|
||||||
t.Errorf("1901/AC4: the output starts with %q, want the cause %q", first, f.Cause)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A package that passes, and a test that fails on its own assertion, keep what they had: no cause is made up for them.
|
|
||||||
func TestAnAssertionKeepsItsOwnCause(t *testing.T) { // 1901/AC4
|
|
||||||
raw, err := os.ReadFile("testdata/captured/go-test-json-log-then-fatal.jsonl")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
res := &TestResult{}
|
|
||||||
rd := newReader(res, ".")
|
|
||||||
rd.read(string(raw))
|
|
||||||
rd.finish()
|
|
||||||
if len(res.Failed) != 1 || !strings.HasSuffix(res.Failed[0].Cause, "3 subscription(s) derive to a subject no emitter owns:") {
|
|
||||||
t.Errorf("1901/AC4: failures = %+v", res.Failed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// go prints the runtime's lines ("panic:", "fatal error:", "signal:", "WARNING: DATA RACE") at column 0: a test's own
|
|
||||||
// log line that holds the same words is indented, and is not the cause (review round 1). The fixture is the deeptimeout
|
|
||||||
// capture with two log lines of the first running test added: a t.Logf and its indented continuation saying "panic:".
|
|
||||||
func TestALoggedPanicIsNotTheCause(t *testing.T) { // 1901/AC4
|
|
||||||
raw, err := os.ReadFile("testdata/captured/go-test-json-pkg-deeptimeout.jsonl")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
lines := strings.SplitAfter(string(raw), "\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"
|
|
||||||
out := strings.Join(lines[:10], "") + added + strings.Join(lines[10:], "")
|
|
||||||
res := &TestResult{}
|
|
||||||
rd := newReader(res, ".")
|
|
||||||
rd.read(out)
|
|
||||||
rd.finish()
|
|
||||||
if len(res.Failed) != 1 || !strings.HasPrefix(res.Failed[0].Cause, "panic: test timed out after 2s") {
|
|
||||||
t.Errorf("1901/AC4: failures = %+v, want the timeout's panic as the cause", res.Failed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A package counts its own skipped tests, so the checker withholds a kept verdict from that package alone (issue 890).
|
|
||||||
func TestEachPackageCountsItsOwnSkippedTests(t *testing.T) {
|
|
||||||
log := `{"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}
|
|
||||||
`
|
|
||||||
res := &TestResult{}
|
|
||||||
rd := newReader(res, ".")
|
|
||||||
rd.read(log)
|
|
||||||
rd.finish()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,9 +41,6 @@ func (m *Module) merge(ctx context.Context, res *FetchResult, tree, remote, base
|
|||||||
}
|
}
|
||||||
res.Base = mr.Base
|
res.Base = mr.Base
|
||||||
res.Conflicts, res.Merge, res.Changed = mr.Conflicts, mr.Merge, mr.Changed
|
res.Conflicts, res.Merge, res.Changed = mr.Conflicts, mr.Merge, mr.Changed
|
||||||
if mr.Merge != "" {
|
|
||||||
res.Programs = &mr.Programs
|
|
||||||
}
|
|
||||||
if res.Merge == "" {
|
if res.Merge == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func TestFetchWithABaseMergesAndAnswersThePreview(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if res.Base != baseSHA || res.Merge != mergeSHA || res.Commit != commitSHA || len(res.Conflicts) != 0 ||
|
if res.Base != baseSHA || res.Merge != mergeSHA || res.Commit != commitSHA || len(res.Conflicts) != 0 ||
|
||||||
strings.Join(res.Changed, ",") != "a.go,modules/m/main.go" || res.Programs == nil || len(*res.Programs) != 0 { // 1577: a merge says it found no program
|
strings.Join(res.Changed, ",") != "a.go,modules/m/main.go" {
|
||||||
t.Errorf("result = %s", st.Result)
|
t.Errorf("result = %s", st.Result)
|
||||||
}
|
}
|
||||||
mt, ok := res.Trees["modules/m"]
|
mt, ok := res.Trees["modules/m"]
|
||||||
|
|||||||
@@ -1,7 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read other catalogue modules' files, outside this module (issue 1935): never kept.
|
||||||
# ? 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.
|
|
||||||
# the manifests through mesh-sdk's validator (the predefined keys), gitea's and mesh-build-node's.
|
|
||||||
predefined.json
|
|
||||||
modules/gitea
|
|
||||||
modules/mesh-build-node
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -181,7 +182,7 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res := &TestResult{Level: o.level, Packages: []PackageResult{}, Setup: o.setup.Seconds(), Store: o.store, Bus: o.bus}
|
res := &TestResult{Level: o.level, Packages: []PackageResult{}, Setup: o.setup.Seconds(), Store: o.store, Bus: o.bus, By: buildstep.By("mesh-build-go")}
|
||||||
byDir, err := packagesByDir(tree, dirs, o.packages)
|
byDir, err := packagesByDir(tree, dirs, o.packages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -194,10 +195,10 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
|||||||
// The directories that hold a selected package run side by side, up to the bound, each into a result of its own;
|
// The directories that hold a selected package run side by side, up to the bound, each into a result of its own;
|
||||||
// the results join in directory order, so the answer does not depend on which ended first (ADR 0536 §3).
|
// the results join in directory order, so the answer does not depend on which ended first (ADR 0536 §3).
|
||||||
type dirRun struct {
|
type dirRun struct {
|
||||||
res *TestResult
|
res *TestResult
|
||||||
rd *reader
|
r runAnswer
|
||||||
r runAnswer
|
noise []string
|
||||||
noise []string
|
packages int
|
||||||
}
|
}
|
||||||
var todo []string
|
var todo []string
|
||||||
for _, d := range dirs {
|
for _, d := range dirs {
|
||||||
@@ -252,9 +253,7 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
|||||||
dr := &runs[i]
|
dr := &runs[i]
|
||||||
dr.res = &TestResult{}
|
dr.res = &TestResult{}
|
||||||
dr.r = run(ctx, append(m.goEnvFor(filepath.Join(tree, d)), o.env...), "go", args...)
|
dr.r = run(ctx, append(m.goEnvFor(filepath.Join(tree, d)), o.env...), "go", args...)
|
||||||
dr.rd = newReader(dr.res, d)
|
dr.noise, dr.packages = readGoTest(dr.res, d, dr.r.Stdout)
|
||||||
dr.noise = dr.rd.read(dr.r.Stdout)
|
|
||||||
dr.rd.finish()
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -263,7 +262,7 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
|||||||
}
|
}
|
||||||
for i, d := range todo {
|
for i, d := range todo {
|
||||||
dr := runs[i]
|
dr := runs[i]
|
||||||
if (dr.r.Status != 0 || dr.r.Err != "") && len(dr.rd.pkgs) == 0 {
|
if (dr.r.Status != 0 || dr.r.Err != "") && dr.packages == 0 {
|
||||||
// go test said nothing a test said: it could not run (a go.mod it cannot read, a toolchain it cannot find).
|
// go test said nothing a test said: it could not run (a go.mod it cannot read, a toolchain it cannot find).
|
||||||
return nil, fmt.Errorf("go test in %s: %s", d, firstLine(dr.r.Err+"\n"+strings.Join(dr.noise, "\n")+"\n"+dr.r.Stderr))
|
return nil, fmt.Errorf("go test in %s: %s", d, firstLine(dr.r.Err+"\n"+strings.Join(dr.noise, "\n")+"\n"+dr.r.Stderr))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,14 +26,6 @@ gunzip and sha256) at origin/main 0205b8f7 on 2026-10-21: directory f8af1d94...d
|
|||||||
055cffc2...428e. The digests above hold the gzip bytes of go1.27.1. The first try of issue 1589 on the merge check's build node answered
|
055cffc2...428e. The digests above hold the gzip bytes of go1.27.1. The first try of issue 1589 on the merge check's build node answered
|
||||||
other digests; the tests therefore compare the stream always and the digest only under go1.27.1.
|
other digests; the tests therefore compare the stream always and the digest only under go1.27.1.
|
||||||
|
|
||||||
go-test-json-pkg-*.jsonl are the standard output and error of `go test -json -count=1 ./...` (go1.27.3, the timeouts with
|
The go-test-json-*.jsonl captures of issue 1901 (2026-10-23 and 2026-10-25) moved to mesh-sdk's conformance kit
|
||||||
`-timeout 2s`), captured on 2026-10-25 for issue 1901 in throwaway one-file modules that each fail without a test's own
|
(go/buildstep/conformance/captured/go, with their description) on 2026-10-26, with the reader they test: the build module
|
||||||
assertion: panicking (a nil map write in TestPanics), timeout (TestWaitsForever sleeps past the timeout), deeptimeout (two
|
reads go test -json through buildstep.ReadGoTest and runs the kit (kit_test.go).
|
||||||
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) and mainexit (TestMain exits 2 after m.Run passed: go test
|
|
||||||
prints "PASS" and then "FAIL <pkg>").
|
|
||||||
go-test-json-pkg-race.jsonl and go-test-json-pkg-racemain.jsonl are the same, with `go test -race -json -count=1 ./...`
|
|
||||||
(CGO_ENABLED=1, go1.27.3, 2026-10-25, issue 1901): race (TestRacesOnTheSeat writes a package variable from itself and a
|
|
||||||
goroutine) and racemain (TestMain does so after m.Run passed: go test prints "PASS", then the race report and "Found 1
|
|
||||||
data race(s)", then "FAIL <pkg>").
|
|
||||||
|
|||||||
Vendored
-13
@@ -1,13 +0,0 @@
|
|||||||
{"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}
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
{"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]"}
|
|
||||||
Vendored
-417
@@ -1,417 +0,0 @@
|
|||||||
{"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}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{"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}
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
{"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}
|
|
||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
{"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}
|
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
{"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}
|
|
||||||
Vendored
-15
@@ -1,15 +0,0 @@
|
|||||||
{"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}
|
|
||||||
-53
@@ -1,53 +0,0 @@
|
|||||||
{"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}
|
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
{"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}
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
{"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}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"capacity": "shanks=3,ace=2,g14=0,novox=0",
|
"capacity": "shanks=3,ace=2,g14=0,novox=0"
|
||||||
"cores-per-step": 6,
|
|
||||||
"build-hours": ""
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module meshbuildgo
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026191742-47a66e3c0dc8
|
||||||
github.com/distribution/distribution/v3 v3.1.2
|
github.com/distribution/distribution/v3 v3.1.2
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536 h1:+SoDdDXF7Tgj38WP4kRpUSps6DJVl6P+w5u/KfdtwnQ=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026191742-47a66e3c0dc8 h1:6OaUWIjcpXOtnu70zeqH0VEaF4oGykrxGpFkaFOzCmY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026191742-47a66e3c0dc8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||||
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 h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
|
|||||||
@@ -29,18 +29,6 @@
|
|||||||
"description": "How many asks of the Go build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
"description": "How many asks of the Go build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
||||||
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
||||||
"default": "shanks=3,ace=2,g14=0,novox=0"
|
"default": "shanks=3,ace=2,g14=0,novox=0"
|
||||||
},
|
|
||||||
"build-hours": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "When each node may take this module's build asks, as node=HH:MM-HH:MM pairs in the node's local time, separated by commas, such as g14=18:00-06:00 (an end before its start crosses midnight). Outside its window a node takes no new step and finishes the ones it runs; a node not named, or an empty value, builds at any hour. The operator, 2026-10-26: g14 builds only outside office hours.",
|
|
||||||
"pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*",
|
|
||||||
"default": ""
|
|
||||||
},
|
|
||||||
"cores-per-step": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "How many processors one Go step of this module needs (a full check of a large repository takes what it is given: three at about 4 cores each ran past the 30-minute step bound on 2026-10-26). The capacity above is an upper bound: on each node the module runs at most floor(the cores of its build slice's CPU quota / this) steps at once, the quota read live from its cgroup (issue 1806), so a lowered build CPU share never starves its steps. Above 0.",
|
|
||||||
"minimum": 0.1,
|
|
||||||
"default": 6
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": [
|
"tools": [
|
||||||
@@ -119,7 +107,7 @@
|
|||||||
"path": "${dir:state}/settings.json",
|
"path": "${dir:state}/settings.json",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"owner": "mesh-build-go",
|
"owner": "mesh-build-go",
|
||||||
"takes-config": ["capacity", "cores-per-step", "build-hours"],
|
"takes-config": ["capacity"],
|
||||||
"merge": "json",
|
"merge": "json",
|
||||||
"template": "files/settings.json"
|
"template": "files/settings.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ The Dockerfile's steps may use the network (ADR 0487 §6); nothing a step can re
|
|||||||
|
|
||||||
- `build_image_fetch` fetches a commit over SSH into a workspace (a base commit makes the merge preview, as the other
|
- `build_image_fetch` fetches a commit over SSH into a workspace (a base commit makes the merge preview, as the other
|
||||||
build modules' fetch). Long: answers a step id and ends on its key in `steps`.
|
build modules' fetch). Long: answers a step id and ends on its key in `steps`.
|
||||||
- `build_image_build` builds one image artifact: `buildah bud` with the Dockerfile `from` names (in the module's
|
- `build_image_build` builds one image artifact: `buildah bud` with the Dockerfile of the module's directory as file
|
||||||
directory or a subdirectory of it) as file and the module's own directory as context, so a COPY names paths from the
|
and context, `--target`, and the images `build.on` pins as `--build-arg`. Answers the image's digest. With
|
||||||
module's root (issue 1703), `--target`, and the images `build.on` pins as `--build-arg`. Answers the image's digest. With
|
|
||||||
`publish: true` (a merged trunk commit only) the module's own process then pushes it to the registry under
|
`publish: true` (a merged trunk commit only) the module's own process then pushes it to the registry under
|
||||||
`<module>/<artifact>:<tag of its own>` (the first 12 hex digits of the digest unless the call gives one), waiting out
|
`<module>/<artifact>:<tag of its own>` (the first 12 hex digits of the digest unless the call gives one), waiting out
|
||||||
a registry held still, and the key carries the digest the registry serves and the reference pinned by it. Without
|
a registry held still, and the key carries the digest the registry serves and the reference pinned by it. Without
|
||||||
|
|||||||
@@ -40,17 +40,9 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
|||||||
// The capacity's own change wakes the puller (issue 1887): the runtime's watch of the config value as mesh-config
|
// The capacity's own change wakes the puller (issue 1887): the runtime's watch of the config value as mesh-config
|
||||||
// resolved it; the settings file is read only when that watch cannot start. A test's Capacity is its own.
|
// resolved it; the settings file is read only when that watch cannot start. A test's Capacity is its own.
|
||||||
var watch func(string, func(stdio.ProvisionChange) error) error
|
var watch func(string, func(stdio.ProvisionChange) error) error
|
||||||
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
|
||||||
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
|
||||||
// And the node's build hours (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00): outside them it takes
|
|
||||||
// nothing new.
|
|
||||||
var cores func() float64
|
|
||||||
var hours func() (*buildstep.BuildWindow, string)
|
|
||||||
if capacity == nil {
|
if capacity == nil {
|
||||||
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
|
||||||
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
||||||
watch = buildstep.WatchConfigServed
|
watch = buildstep.WatchConfigServed
|
||||||
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskImage], m.Say)
|
|
||||||
}
|
}
|
||||||
ask := m.askSteps(steps)
|
ask := m.askSteps(steps)
|
||||||
var load func(string) ([]byte, error)
|
var load func(string) ([]byte, error)
|
||||||
@@ -58,7 +50,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
|||||||
load = func(key string) ([]byte, error) { return m.ReadPauses.Get(key) }
|
load = func(key string) ([]byte, error) { return m.ReadPauses.Get(key) }
|
||||||
}
|
}
|
||||||
return &buildstep.Puller{Source: source, Language: delivery.AskImage, Node: m.Node, Put: m.Steps, PausePut: m.Pauses, Load: load,
|
return &buildstep.Puller{Source: source, Language: delivery.AskImage, Node: m.Node, Put: m.Steps, PausePut: m.Pauses, Load: load,
|
||||||
Capacity: capacity, WatchConfig: watch, CoresPerStep: cores, BuildHours: hours, Logf: m.Say,
|
Capacity: capacity, WatchConfig: watch, Logf: m.Say,
|
||||||
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
|
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
|
||||||
if err := m.Check(ctx); err != nil {
|
if err := m.Check(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -55,18 +55,18 @@ func (m *Module) buildTool() StepTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// unsupportedImage says why this module does not build the artifact, or "" when it does. It is the one place that
|
// unsupportedImage says why this module does not build the artifact, or "" when it does. It is the one place that
|
||||||
// knows which image forms are out: ADR 0487 §1 builds from the module's own directory (the build context) and the base
|
// knows which image forms are out: ADR 0487 §1 takes the Dockerfile's directory as the build context and the base
|
||||||
// from the Dockerfile or an image pinned in `build.on`, so a context that is another repository and a base that is
|
// from the Dockerfile or an image pinned in `build.on`, so a context that is another repository and a base that is
|
||||||
// another module's artifact are not built (the lead's decision for R4c-9, which retires them with build-agent at R4g).
|
// another module's artifact are not built (the lead's decision for R4c-9, which retires them with build-agent at R4g).
|
||||||
func unsupportedImage(manifest catalogue.Manifest, a catalogue.Artifact) string {
|
func unsupportedImage(manifest catalogue.Manifest, a catalogue.Artifact) string {
|
||||||
if a.Context != nil {
|
if a.Context != nil {
|
||||||
return fmt.Sprintf("the image %s of %s builds with the repository %s as its context; mesh-build-image takes the module's own directory as the build context (ADR 0487 §1) and does not build another repository",
|
return fmt.Sprintf("the image %s of %s builds with the repository %s as its context; mesh-build-image takes the Dockerfile's directory as the build context (ADR 0487 §1) and does not build a whole repository",
|
||||||
a.Name, manifest.Module, a.Context.Repository)
|
a.Name, manifest.Module, a.Context.Repository)
|
||||||
}
|
}
|
||||||
if manifest.Build != nil {
|
if manifest.Build != nil {
|
||||||
for _, on := range manifest.Build.On {
|
for _, on := range manifest.Build.On {
|
||||||
if on.Module != "" {
|
if on.Module != "" {
|
||||||
return fmt.Sprintf("the image %s of %s stands on the artifact %s of the module %s through the build argument %s; mesh-build-image builds only from the module's own directory and an image pinned in build.on (ADR 0487 §1)",
|
return fmt.Sprintf("the image %s of %s stands on the artifact %s of the module %s through the build argument %s; mesh-build-image builds only from the Dockerfile's directory and an image pinned in build.on (ADR 0487 §1)",
|
||||||
a.Name, manifest.Module, on.Artifact, on.Module, on.Arg)
|
a.Name, manifest.Module, on.Artifact, on.Module, on.Arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,14 +183,8 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
return m.push(ctx, workspace, manifest.Module, local, res, tag)
|
return m.push(ctx, workspace, manifest.Module, local, res, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dockerfileOf is the Dockerfile an artifact's `from` names and the build context: the module's own directory, the
|
// dockerfileOf is the Dockerfile an artifact's `from` names and the build context: the directory it lies in. It must
|
||||||
// one its manifest lies in, whichever subdirectory the Dockerfile sits in. `from` is a path relative to that
|
// be a regular file inside the module's own directory (checks/imageplace holds the catalogue to the same).
|
||||||
// directory, and so is every COPY of the Dockerfile: a recipe in a subdirectory (photos' admin-client/Dockerfile
|
|
||||||
// copies admin-client/... and client-server/serve.mjs) builds as build-agent built it, and as the fingerprint reads
|
|
||||||
// the module's whole directory as its build source. Taking the Dockerfile's own directory instead failed every such
|
|
||||||
// COPY with `copier: stat ... no such file or directory` (issue 1703, 2026-10-26). The context never leaves the module's
|
|
||||||
// directory, so a module's build reads no other module's files. The Dockerfile must be a regular file inside that
|
|
||||||
// directory (checks/imageplace holds the catalogue to the same).
|
|
||||||
func dockerfileOf(moduleDir, from string) (dockerfile, contextDir string, err error) {
|
func dockerfileOf(moduleDir, from string) (dockerfile, contextDir string, err error) {
|
||||||
clean := filepath.Clean(filepath.FromSlash(strings.TrimSpace(from)))
|
clean := filepath.Clean(filepath.FromSlash(strings.TrimSpace(from)))
|
||||||
if from == "" || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
|
if from == "" || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
|
||||||
@@ -201,7 +195,7 @@ func dockerfileOf(moduleDir, from string) (dockerfile, contextDir string, err er
|
|||||||
if err != nil || !info.Mode().IsRegular() {
|
if err != nil || !info.Mode().IsRegular() {
|
||||||
return "", "", fmt.Errorf("%q is not a file in the module's directory", from)
|
return "", "", fmt.Errorf("%q is not a file in the module's directory", from)
|
||||||
}
|
}
|
||||||
return dockerfile, moduleDir, nil
|
return dockerfile, filepath.Dir(dockerfile), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// localName names the image in buildah's store: one name for each artifact of a workspace.
|
// localName names the image in buildah's store: one name for each artifact of a workspace.
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func TestBuildRunsBuildahRootlessAndAnswersTheDigest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(bud[0].line, " "+dir) {
|
if !strings.HasSuffix(bud[0].line, " "+dir) {
|
||||||
t.Errorf("the context is not the module's directory:\n%s", bud[0].line)
|
t.Errorf("the context is not the Dockerfile's directory:\n%s", bud[0].line)
|
||||||
}
|
}
|
||||||
for _, c := range f.calls {
|
for _, c := range f.calls {
|
||||||
if c.name != "buildah" {
|
if c.name != "buildah" {
|
||||||
@@ -122,43 +122,6 @@ func TestBuildRunsBuildahRootlessAndAnswersTheDigest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue 1703, 2026-10-26: a Dockerfile in a subdirectory of the module builds with the module's directory as its
|
|
||||||
// context, so its COPY of a path from the module's root (photos' admin-client/Dockerfile copies admin-client/... and
|
|
||||||
// client-server/serve.mjs) finds it; the Dockerfile's own directory as the context failed with `copier: stat
|
|
||||||
// "/admin-client/package.json": no such file or directory`.
|
|
||||||
func TestBuildContextIsTheModulesDirectoryForADockerfileInASubdirectory(t *testing.T) {
|
|
||||||
m, f, states := setup(t, func(s string) string {
|
|
||||||
return strings.Replace(s, `"from": "Dockerfile"`, `"from": "admin-client/Dockerfile"`, 1)
|
|
||||||
})
|
|
||||||
dir := filepath.Join(m.Work, "ws1", "tree", "modules", "demo")
|
|
||||||
for name, body := range map[string]string{
|
|
||||||
"admin-client/Dockerfile": "ARG DEMO_BASE\nFROM ${DEMO_BASE} AS final\nCOPY admin-client/package.json client-server/serve.mjs /\n",
|
|
||||||
"admin-client/package.json": "{}\n",
|
|
||||||
"client-server/serve.mjs": "\n",
|
|
||||||
} {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, st := runBuild(t, m, states, nil)
|
|
||||||
if st.State != delivery.StepPassed {
|
|
||||||
t.Fatalf("step = %+v", st)
|
|
||||||
}
|
|
||||||
bud := f.called("bud")
|
|
||||||
if len(bud) != 1 {
|
|
||||||
t.Fatalf("buildah bud ran %d times", len(bud))
|
|
||||||
}
|
|
||||||
if !strings.Contains(bud[0].line, "--file "+dir+"/admin-client/Dockerfile ") {
|
|
||||||
t.Errorf("the Dockerfile is not the subdirectory's:\n%s", bud[0].line)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(bud[0].line, " "+dir) {
|
|
||||||
t.Errorf("the context is not the module's directory, so admin-client/ and client-server/ are out of reach:\n%s", bud[0].line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1703c, issue 1703: a failing Dockerfile step fails the step, its reason saying buildah's error from stderr, never the
|
// 1703c, issue 1703: a failing Dockerfile step fails the step, its reason saying buildah's error from stderr, never the
|
||||||
// last STEP line buildah writes on stdout; nothing is pushed.
|
// last STEP line buildah writes on stdout; nothing is pushed.
|
||||||
func TestBuildFailsWithBuildahsError(t *testing.T) {
|
func TestBuildFailsWithBuildahsError(t *testing.T) {
|
||||||
@@ -215,7 +178,7 @@ func TestBuildRefusesAnImageWithARepositoryContextOrAModuleBase(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
"context": {func(s string) string {
|
"context": {func(s string) string {
|
||||||
return strings.Replace(s, `"target": "final"`, `"target": "final", "context": {"seat": "git", "repository": "novox/mesh-controller"}`, 1)
|
return strings.Replace(s, `"target": "final"`, `"target": "final", "context": {"seat": "git", "repository": "novox/mesh-controller"}`, 1)
|
||||||
}, []string{"novox/mesh-controller", "module's own directory", "ADR 0487"}},
|
}, []string{"novox/mesh-controller", "Dockerfile's directory", "ADR 0487"}},
|
||||||
"module base": {func(s string) string {
|
"module base": {func(s string) string {
|
||||||
return strings.Replace(s, `"image": "docker.io/library/busybox@sha256:0000000000000000000000000000000000000000000000000000000000000001"`, `"module": "mesh-sdk", "artifact": "lib"`, 1)
|
return strings.Replace(s, `"image": "docker.io/library/busybox@sha256:0000000000000000000000000000000000000000000000000000000000000001"`, `"module": "mesh-sdk", "artifact": "lib"`, 1)
|
||||||
}, []string{"artifact lib of the module mesh-sdk", "DEMO_BASE"}},
|
}, []string{"artifact lib of the module mesh-sdk", "DEMO_BASE"}},
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import (
|
|||||||
func (m *Module) cpuLimits() buildcpu.Limits {
|
func (m *Module) cpuLimits() buildcpu.Limits {
|
||||||
slots := 1
|
slots := 1
|
||||||
switch {
|
switch {
|
||||||
case m.Held != nil:
|
|
||||||
slots = m.Held.CapacityNow() // the capacity the puller judges by, bounded by the node's build slice
|
|
||||||
case m.Capacity != nil:
|
case m.Capacity != nil:
|
||||||
slots = m.Capacity()
|
slots = m.Capacity()
|
||||||
case m.Settings != "":
|
case m.Settings != "":
|
||||||
@@ -34,9 +32,5 @@ func (m *Module) threadEnv() []string {
|
|||||||
// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime
|
// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime
|
||||||
// to (its unit and its slice), read from its cgroup rather than assumed.
|
// to (its unit and its slice), read from its cgroup rather than assumed.
|
||||||
func (m *Module) cpuStatus() map[string]any {
|
func (m *Module) cpuStatus() map[string]any {
|
||||||
out := map[string]any{"limits": m.cpuLimits(), "in-force": buildcpu.ReadInForce()}
|
return map[string]any{"limits": m.cpuLimits(), "in-force": buildcpu.ReadInForce()}
|
||||||
if m.Held != nil {
|
|
||||||
out["capacity"] = m.Held.CapacityDetail() // the capacity judged by and why (issue 1806's follow-up)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,15 +31,13 @@ const ForgeAlias = "mesh-forge"
|
|||||||
// files in conflict, the paths the merge changes and the tree of each module directory). A head that does not
|
// files in conflict, the paths the merge changes and the tree of each module directory). A head that does not
|
||||||
// merge cleanly is an answer: Merge is empty, Conflicts names the files, and the workspace is left at the trunk.
|
// merge cleanly is an answer: Merge is empty, Conflicts names the files, and the workspace is left at the trunk.
|
||||||
type FetchResult struct {
|
type FetchResult struct {
|
||||||
Workspace string `json:"workspace"`
|
Workspace string `json:"workspace"`
|
||||||
Commit string `json:"commit"`
|
Commit string `json:"commit"`
|
||||||
Base string `json:"base,omitempty"`
|
Base string `json:"base,omitempty"`
|
||||||
Merge string `json:"merge,omitempty"`
|
Merge string `json:"merge,omitempty"`
|
||||||
Conflicts []string `json:"conflicts,omitempty"`
|
Conflicts []string `json:"conflicts,omitempty"`
|
||||||
Changed []string `json:"changed,omitempty"`
|
Changed []string `json:"changed,omitempty"`
|
||||||
// Programs are the changed files that are compiled programs (issue 1577); a merge answers it, a pointer to an empty list when there are none, absent for a fetch that made no merge.
|
Trees map[string]buildstep.ModuleTree `json:"trees,omitempty"`
|
||||||
Programs *[]buildstep.Program `json:"programs,omitempty"`
|
|
||||||
Trees map[string]buildstep.ModuleTree `json:"trees,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) fetchTool() StepTool {
|
func (m *Module) fetchTool() StepTool {
|
||||||
@@ -129,9 +127,6 @@ func (m *Module) merge(ctx context.Context, res *FetchResult, tree, remote, base
|
|||||||
}
|
}
|
||||||
res.Base = mr.Base
|
res.Base = mr.Base
|
||||||
res.Conflicts, res.Merge, res.Changed = mr.Conflicts, mr.Merge, mr.Changed
|
res.Conflicts, res.Merge, res.Changed = mr.Conflicts, mr.Merge, mr.Changed
|
||||||
if mr.Merge != "" {
|
|
||||||
res.Programs = &mr.Programs
|
|
||||||
}
|
|
||||||
if res.Merge == "" {
|
if res.Merge == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read other catalogue modules' files, outside this module (issue 1935): never kept.
|
||||||
# ? 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.
|
|
||||||
# the manifests through mesh-sdk's validator (the predefined keys), with the modules they compose with.
|
|
||||||
predefined.json
|
|
||||||
modules/gitea
|
|
||||||
modules/mesh-build-go
|
|
||||||
modules/mesh-build-node
|
|
||||||
modules/buildah
|
|
||||||
modules/docker
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"capacity": "shanks=3,ace=2,g14=0,novox=0",
|
"capacity": "shanks=3,ace=2,g14=0,novox=0"
|
||||||
"cores-per-step": 2,
|
|
||||||
"build-hours": ""
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module meshbuildimage
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536 h1:+SoDdDXF7Tgj38WP4kRpUSps6DJVl6P+w5u/KfdtwnQ=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f h1:GYLCvz2x+JNUOTbXzyh2QwzX0sTzvRlXR7lGocWjk3A=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f/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 h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
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 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -24,18 +24,6 @@
|
|||||||
"description": "How many asks of the image build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
"description": "How many asks of the image build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
||||||
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
||||||
"default": "shanks=3,ace=2,g14=0,novox=0"
|
"default": "shanks=3,ace=2,g14=0,novox=0"
|
||||||
},
|
|
||||||
"build-hours": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "When each node may take this module's build asks, as node=HH:MM-HH:MM pairs in the node's local time, separated by commas, such as g14=18:00-06:00 (an end before its start crosses midnight). Outside its window a node takes no new step and finishes the ones it runs; a node not named, or an empty value, builds at any hour. The operator, 2026-10-26: g14 builds only outside office hours.",
|
|
||||||
"pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*",
|
|
||||||
"default": ""
|
|
||||||
},
|
|
||||||
"cores-per-step": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "How many processors one image step of this module needs (buildah, whose RUN lines are mostly fetches). The capacity above is an upper bound: on each node the module runs at most floor(the cores of its build slice's CPU quota / this) steps at once, the quota read live from its cgroup (issue 1806), so a lowered build CPU share never starves its steps. Above 0.",
|
|
||||||
"minimum": 0.1,
|
|
||||||
"default": 2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": [
|
"tools": [
|
||||||
@@ -90,7 +78,7 @@
|
|||||||
"path": "${dir:state}/settings.json",
|
"path": "${dir:state}/settings.json",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"owner": "mesh-build-image",
|
"owner": "mesh-build-image",
|
||||||
"takes-config": ["capacity", "cores-per-step", "build-hours"],
|
"takes-config": ["capacity"],
|
||||||
"merge": "json",
|
"merge": "json",
|
||||||
"template": "files/settings.json"
|
"template": "files/settings.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,17 +48,9 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
|||||||
// The capacity's own change wakes the puller (issue 1887): the runtime's watch of the config value as mesh-config
|
// The capacity's own change wakes the puller (issue 1887): the runtime's watch of the config value as mesh-config
|
||||||
// resolved it; the settings file is read only when that watch cannot start. A test's Capacity is its own.
|
// resolved it; the settings file is read only when that watch cannot start. A test's Capacity is its own.
|
||||||
var watch func(string, func(stdio.ProvisionChange) error) error
|
var watch func(string, func(stdio.ProvisionChange) error) error
|
||||||
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
|
||||||
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
|
||||||
// And the node's build hours (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00): outside them it takes
|
|
||||||
// nothing new.
|
|
||||||
var cores func() float64
|
|
||||||
var hours func() (*buildstep.BuildWindow, string)
|
|
||||||
if capacity == nil {
|
if capacity == nil {
|
||||||
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
|
||||||
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
||||||
watch = buildstep.WatchConfigServed
|
watch = buildstep.WatchConfigServed
|
||||||
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskNode], m.Say)
|
|
||||||
}
|
}
|
||||||
ask := m.askSteps(steps)
|
ask := m.askSteps(steps)
|
||||||
var load func(string) ([]byte, error)
|
var load func(string) ([]byte, error)
|
||||||
@@ -66,7 +58,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
|||||||
load = func(key string) ([]byte, error) { return m.ReadPauses.Get(key) }
|
load = func(key string) ([]byte, error) { return m.ReadPauses.Get(key) }
|
||||||
}
|
}
|
||||||
return &buildstep.Puller{Source: source, Language: delivery.AskNode, Node: m.Node, Put: m.Steps, PausePut: m.Pauses, Load: load,
|
return &buildstep.Puller{Source: source, Language: delivery.AskNode, Node: m.Node, Put: m.Steps, PausePut: m.Pauses, Load: load,
|
||||||
Capacity: capacity, WatchConfig: watch, CoresPerStep: cores, BuildHours: hours, Logf: m.Say,
|
Capacity: capacity, WatchConfig: watch, Logf: m.Say,
|
||||||
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
|
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
|
||||||
if err := m.Prepare(ctx); err != nil {
|
if err := m.Prepare(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -84,28 +83,12 @@ func TestManifestTakesTheBuildCapacity(t *testing.T) {
|
|||||||
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
||||||
var doc struct {
|
var doc struct {
|
||||||
Config map[string]struct {
|
Config map[string]struct {
|
||||||
Default json.RawMessage `json:"default"`
|
Default string `json:"default"`
|
||||||
} `json:"config"`
|
} `json:"config"`
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(raw, &doc)
|
_ = json.Unmarshal(raw, &doc)
|
||||||
// Issue 1806's follow-up: the cores a step needs is a setting of its own, in the settings file, with the SDK's default.
|
if doc.Config[buildstep.CapacitySetting].Default != buildstep.DefaultCapacities {
|
||||||
if cores := string(doc.Config[buildstep.CoresPerStepSetting].Default); cores != fmt.Sprint(buildstep.DefaultCoresPerStep[delivery.AskNode]) {
|
t.Errorf("the capacity's default = %q", doc.Config[buildstep.CapacitySetting].Default)
|
||||||
t.Errorf("the cores-per-step default = %s, want %g", cores, buildstep.DefaultCoresPerStep[delivery.AskNode])
|
|
||||||
}
|
|
||||||
if got := buildstep.NodeCoresPerStep(moduleDir+"/files/settings.json", 0, nil)(); got != buildstep.DefaultCoresPerStep[delivery.AskNode] {
|
|
||||||
t.Errorf("the settings template holds cores-per-step %g", got)
|
|
||||||
}
|
|
||||||
// And the build hours (the operator, 2026-10-26): a setting of their own, empty by default (any hour), in the template.
|
|
||||||
if hours := string(doc.Config[buildstep.BuildHoursSetting].Default); hours != `""` {
|
|
||||||
t.Errorf("the build-hours default = %s, want empty", hours)
|
|
||||||
}
|
|
||||||
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
|
|
||||||
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
|
|
||||||
}
|
|
||||||
var capacity string
|
|
||||||
_ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity)
|
|
||||||
if capacity != buildstep.DefaultCapacities {
|
|
||||||
t.Errorf("the capacity's default = %q", capacity)
|
|
||||||
}
|
}
|
||||||
if resource(t, m, "settings")["path"] != "${dir:state}/settings.json" {
|
if resource(t, m, "settings")["path"] != "${dir:state}/settings.json" {
|
||||||
t.Error("no settings file")
|
t.Error("no settings file")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,14 +55,15 @@ type TestResult struct {
|
|||||||
// the catalogue's TypeScript against the bounds is a later release's (R4f), not this one's gate.
|
// the catalogue's TypeScript against the bounds is a later release's (R4f), not this one's gate.
|
||||||
OverBound bool `json:"over_bound"`
|
OverBound bool `json:"over_bound"`
|
||||||
Failed []Failure `json:"failed,omitempty"`
|
Failed []Failure `json:"failed,omitempty"`
|
||||||
|
// By is the build module and the SDK it was built with (buildstep.By): the checker names it when a failure hides
|
||||||
|
// its cause.
|
||||||
|
By string `json:"by,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Failure is a check or script that failed; Output is the first lines it wrote.
|
// Failure is the SDK's failure contract (buildstep.Failure, issue 1901): a failing test of the test script, read from
|
||||||
type Failure struct {
|
// node's TAP reporter, or a type-check or build that failed, each with its cause (never empty, never a passing line) and
|
||||||
Package string `json:"package"`
|
// its own output up to 4096 bytes, cause first.
|
||||||
Test string `json:"test,omitempty"`
|
type Failure = buildstep.Failure
|
||||||
Output string `json:"output,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) testTool() StepTool {
|
func (m *Module) testTool() StepTool {
|
||||||
return StepTool{
|
return StepTool{
|
||||||
@@ -104,7 +106,7 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res := &TestResult{Workspace: workspace, Module: module, Level: level}
|
res := &TestResult{Workspace: workspace, Module: module, Level: level, By: buildstep.By("mesh-build-node")}
|
||||||
script := "test"
|
script := "test"
|
||||||
if level == "integration" {
|
if level == "integration" {
|
||||||
script = "test:integration"
|
script = "test:integration"
|
||||||
@@ -128,12 +130,12 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
var check, run time.Duration
|
var check, run time.Duration
|
||||||
var failed []Failure
|
var failed []Failure
|
||||||
// a command that could not start is not a verdict: the step fails. One that ran and said no is.
|
// a command that could not start is not a verdict: the step fails. One that ran and said no is.
|
||||||
ran := func(name string, r forgekey.Ran) error {
|
ran := func(name string, r forgekey.Ran, report string) error {
|
||||||
if r.Err != "" {
|
if r.Err != "" {
|
||||||
return fmt.Errorf("%s could not be run: status %d %s: %s", name, r.Status, r.Err, firstLine(r.Stderr))
|
return fmt.Errorf("%s could not be run: status %d %s: %s", name, r.Status, r.Err, firstLine(r.Stderr))
|
||||||
}
|
}
|
||||||
if r.Status != 0 {
|
if r.Status != 0 {
|
||||||
failed = append(failed, Failure{Package: where, Test: name, Output: lines(r.Stdout + "\n" + r.Stderr)})
|
failed = append(failed, testFailures(where, name, dir, r, report)...)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -148,17 +150,23 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
start := m.now()
|
start := m.now()
|
||||||
r := m.runIn(ctx, root, false, dir, nil, "node", line...)
|
r := m.runIn(ctx, root, false, dir, nil, "node", line...)
|
||||||
check = m.now().Sub(start)
|
check = m.now().Sub(start)
|
||||||
if err := ran(name, r); err != nil {
|
if err := ran(name, r, ""); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res.Ran = append(res.Ran, name)
|
res.Ran = append(res.Ran, name)
|
||||||
}
|
}
|
||||||
if hasScript && len(failed) == 0 {
|
if hasScript && len(failed) == 0 {
|
||||||
start := m.now()
|
start := m.now()
|
||||||
env := []string{"PATH=" + binDir(root) + ":" + pathOrDefault(), "npm_config_cache=" + filepath.Join(m.Toolchain.Paths.Cache, "npm")}
|
// node --test writes its TAP report to a file of the workspace (issue 1901), whatever the script prints itself
|
||||||
|
report := filepath.Join(root, ".mesh-node-test-"+level+".tap")
|
||||||
|
_ = os.Remove(report)
|
||||||
|
env := []string{"PATH=" + binDir(root) + ":" + pathOrDefault(), "npm_config_cache=" + filepath.Join(m.Toolchain.Paths.Cache, "npm"),
|
||||||
|
"NODE_OPTIONS=" + buildstep.NodeTestOptions(report)}
|
||||||
r := m.runIn(ctx, root, false, dir, env, "npm", scriptArgs(script)...)
|
r := m.runIn(ctx, root, false, dir, env, "npm", scriptArgs(script)...)
|
||||||
run = m.now().Sub(start)
|
run = m.now().Sub(start)
|
||||||
if err := ran(script, r); err != nil {
|
tap, _ := os.ReadFile(report)
|
||||||
|
_ = os.Remove(report)
|
||||||
|
if err := ran(script, r, string(tap)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res.Ran = append(res.Ran, script)
|
res.Ran = append(res.Ran, script)
|
||||||
@@ -166,7 +174,7 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
res.Failed = failed
|
res.Failed = failed
|
||||||
var why []string
|
var why []string
|
||||||
for _, f := range failed {
|
for _, f := range failed {
|
||||||
why = append(why, fmt.Sprintf("%s failed in %s: %s", f.Test, f.Package, firstLine(f.Output)))
|
why = append(why, failureWhy(f))
|
||||||
}
|
}
|
||||||
if level == "unit" {
|
if level == "unit" {
|
||||||
res.Elapsed, res.Bound = (check + run).Seconds(), UnitBound.Seconds()
|
res.Elapsed, res.Bound = (check + run).Seconds(), UnitBound.Seconds()
|
||||||
@@ -188,18 +196,23 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// lines is the first lines of a command's output.
|
// testFailures is the module's parse path of a command that failed (the conformance kit runs it, issue 1901): the test
|
||||||
func lines(out string) string {
|
// script's failing tests from node's TAP report, or, for the type-check or the build, or a script whose report names no
|
||||||
var keep []string
|
// failing test, the command's own output with the line that names its error as the cause.
|
||||||
for _, l := range strings.Split(out, "\n") {
|
func testFailures(where, name, dir string, r forgekey.Ran, report string) []Failure {
|
||||||
if l = strings.TrimSpace(l); l != "" {
|
if name == "test" || name == "test:integration" {
|
||||||
keep = append(keep, l)
|
return buildstep.ReadTAP(where, name, dir, report, r.Stdout+"\n"+r.Stderr)
|
||||||
if len(keep) == 15 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return strings.Join(keep, "\n")
|
return []Failure{buildstep.ConsoleFailure(where, name, r.Stdout+"\n"+r.Stderr)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// failureWhy is the words of one failure: the test or check that failed, where, and its cause.
|
||||||
|
func failureWhy(f Failure) string {
|
||||||
|
file := ""
|
||||||
|
if f.File != "" {
|
||||||
|
file = " (" + f.File + ")"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s failed in %s%s: %s", f.Test, f.Package, file, firstLine(f.Cause))
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstLine(s string) string {
|
func firstLine(s string) string {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -198,4 +199,8 @@ func TestTheTestScriptFindsTscAndEsbuildOnItsPath(t *testing.T) {
|
|||||||
if npm.name != "npm" || !strings.HasPrefix(path, bin+":") {
|
if npm.name != "npm" || !strings.HasPrefix(path, bin+":") {
|
||||||
t.Errorf("npm ran with PATH %q, want %s first", path, bin)
|
t.Errorf("npm ran with PATH %q, want %s first", path, bin)
|
||||||
}
|
}
|
||||||
|
// 1901/AC7: the test script runs under node's TAP reporter, written to a file of the workspace the step reads
|
||||||
|
if !slices.Contains(npm.env, "NODE_OPTIONS="+buildstep.NodeTestOptions(filepath.Join(work, "w1", ".mesh-node-test-unit.tap"))) {
|
||||||
|
t.Errorf("npm ran with %v, want NODE_OPTIONS naming the TAP reporter and its file", npm.env)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import (
|
|||||||
func (m *Module) cpuLimits() buildcpu.Limits {
|
func (m *Module) cpuLimits() buildcpu.Limits {
|
||||||
slots := 1
|
slots := 1
|
||||||
switch {
|
switch {
|
||||||
case m.Held != nil:
|
|
||||||
slots = m.Held.CapacityNow() // the capacity the puller judges by, bounded by the node's build slice
|
|
||||||
case m.Capacity != nil:
|
case m.Capacity != nil:
|
||||||
slots = m.Capacity()
|
slots = m.Capacity()
|
||||||
case m.Settings != "":
|
case m.Settings != "":
|
||||||
@@ -34,9 +32,5 @@ func (m *Module) threadEnv() []string {
|
|||||||
// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime
|
// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime
|
||||||
// to (its unit and its slice), read from its cgroup rather than assumed.
|
// to (its unit and its slice), read from its cgroup rather than assumed.
|
||||||
func (m *Module) cpuStatus() map[string]any {
|
func (m *Module) cpuStatus() map[string]any {
|
||||||
out := map[string]any{"limits": m.cpuLimits(), "in-force": buildcpu.ReadInForce()}
|
return map[string]any{"limits": m.cpuLimits(), "in-force": buildcpu.ReadInForce()}
|
||||||
if m.Held != nil {
|
|
||||||
out["capacity"] = m.Held.CapacityDetail() // the capacity judged by and why (issue 1806's follow-up)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,13 +51,11 @@ type FetchResult struct {
|
|||||||
// commit, the merge commit of the head onto it, the paths the merge changes and the tree of each module directory.
|
// commit, the merge commit of the head onto it, the paths the merge changes and the tree of each module directory.
|
||||||
// A head that does not merge cleanly is an answer: Merge is empty, Conflicts names the files, and the workspace is
|
// A head that does not merge cleanly is an answer: Merge is empty, Conflicts names the files, and the workspace is
|
||||||
// left at the trunk commit with nothing installed.
|
// left at the trunk commit with nothing installed.
|
||||||
Base string `json:"base,omitempty"`
|
Base string `json:"base,omitempty"`
|
||||||
Merge string `json:"merge,omitempty"`
|
Merge string `json:"merge,omitempty"`
|
||||||
Conflicts []string `json:"conflicts,omitempty"`
|
Conflicts []string `json:"conflicts,omitempty"`
|
||||||
Changed []string `json:"changed,omitempty"`
|
Changed []string `json:"changed,omitempty"`
|
||||||
// Programs are the changed files that are compiled programs (issue 1577); a merge answers it, a pointer to an empty list when there are none, absent for a fetch that made no merge.
|
Trees map[string]buildstep.ModuleTree `json:"trees,omitempty"`
|
||||||
Programs *[]buildstep.Program `json:"programs,omitempty"`
|
|
||||||
Trees map[string]buildstep.ModuleTree `json:"trees,omitempty"`
|
|
||||||
// Group is the group's package the fetch packed and served to the TypeScript members (the call's `package`):
|
// Group is the group's package the fetch packed and served to the TypeScript members (the call's `package`):
|
||||||
// "<name>@<version> <integrity>".
|
// "<name>@<version> <integrity>".
|
||||||
Group string `json:"group,omitempty"`
|
Group string `json:"group,omitempty"`
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1901/AC7: the module's own parse path (testFailures, as the test step calls it on a command that failed) keeps the
|
||||||
|
// failure contract on every captured node --test and tsc case of the SDK's conformance kit.
|
||||||
|
func TestTheTestStepKeepsTheFailureContract(t *testing.T) {
|
||||||
|
conformance.Run(t, conformance.NodeTest, func(o conformance.Output) []buildstep.Failure {
|
||||||
|
return testFailures(".", "test", o.Dir, forgekey.Ran{Status: 1, Stdout: o.Stdout, Stderr: o.Stderr}, o.Report)
|
||||||
|
})
|
||||||
|
conformance.Run(t, conformance.TypeCheck, func(o conformance.Output) []buildstep.Failure {
|
||||||
|
return testFailures(".", "type-check", o.Dir, forgekey.Ran{Status: 2, Stdout: o.Stdout, Stderr: o.Stderr}, "")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -27,9 +27,6 @@ func (m *Module) merge(ctx context.Context, run forgekey.Runner, res *FetchResul
|
|||||||
}
|
}
|
||||||
res.Base = mr.Base
|
res.Base = mr.Base
|
||||||
res.Conflicts, res.Merge, res.Changed = mr.Conflicts, mr.Merge, mr.Changed
|
res.Conflicts, res.Merge, res.Changed = mr.Conflicts, mr.Merge, mr.Changed
|
||||||
if mr.Merge != "" {
|
|
||||||
res.Programs = &mr.Programs
|
|
||||||
}
|
|
||||||
if res.Merge == "" {
|
if res.Merge == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
These tests read other catalogue modules' files, outside this module (issue 1935): never kept.
|
||||||
# ? 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.
|
|
||||||
# its manifest through mesh-sdk's validator, which reads the root's predefined keys.
|
|
||||||
predefined.json
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"capacity": "shanks=3,ace=2,g14=0,novox=0",
|
"capacity": "shanks=3,ace=2,g14=0,novox=0"
|
||||||
"cores-per-step": 2,
|
|
||||||
"build-hours": ""
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module meshbuildnode
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026191742-47a66e3c0dc8
|
||||||
github.com/distribution/distribution/v3 v3.1.2
|
github.com/distribution/distribution/v3 v3.1.2
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536 h1:+SoDdDXF7Tgj38WP4kRpUSps6DJVl6P+w5u/KfdtwnQ=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026191742-47a66e3c0dc8 h1:6OaUWIjcpXOtnu70zeqH0VEaF4oGykrxGpFkaFOzCmY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026191742-47a66e3c0dc8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||||
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 h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
|
|||||||
@@ -25,18 +25,6 @@
|
|||||||
"description": "How many asks of the Node build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
"description": "How many asks of the Node build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
||||||
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
||||||
"default": "shanks=3,ace=2,g14=0,novox=0"
|
"default": "shanks=3,ace=2,g14=0,novox=0"
|
||||||
},
|
|
||||||
"build-hours": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "When each node may take this module's build asks, as node=HH:MM-HH:MM pairs in the node's local time, separated by commas, such as g14=18:00-06:00 (an end before its start crosses midnight). Outside its window a node takes no new step and finishes the ones it runs; a node not named, or an empty value, builds at any hour. The operator, 2026-10-26: g14 builds only outside office hours.",
|
|
||||||
"pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*",
|
|
||||||
"default": ""
|
|
||||||
},
|
|
||||||
"cores-per-step": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "How many processors one Node step of this module needs (tsc, esbuild and a test runner; a median step takes 15 s). The capacity above is an upper bound: on each node the module runs at most floor(the cores of its build slice's CPU quota / this) steps at once, the quota read live from its cgroup (issue 1806), so a lowered build CPU share never starves its steps. Above 0.",
|
|
||||||
"minimum": 0.1,
|
|
||||||
"default": 2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": [
|
"tools": [
|
||||||
@@ -112,7 +100,7 @@
|
|||||||
"path": "${dir:state}/settings.json",
|
"path": "${dir:state}/settings.json",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"owner": "mesh-build-node",
|
"owner": "mesh-build-node",
|
||||||
"takes-config": ["capacity", "cores-per-step", "build-hours"],
|
"takes-config": ["capacity"],
|
||||||
"merge": "json",
|
"merge": "json",
|
||||||
"template": "files/settings.json"
|
"template": "files/settings.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func resolving(c *Config) {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, read := range []string{AssignmentsRead, PlacementRead, DeclaredConfigRead, PredefinedRead} {
|
for _, read := range []string{AssignmentsRead, DeclaredConfigRead, PredefinedRead} {
|
||||||
go func(read string) {
|
go func(read string) {
|
||||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||||
time.Sleep(wait)
|
time.Sleep(wait)
|
||||||
|
|||||||
@@ -21,16 +21,4 @@ func TestAModulePulledInHasItsValuesResolvedOnItsNode(t *testing.T) {
|
|||||||
if r, _ := c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
|
if r, _ := c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
|
||||||
t.Errorf("a module no longer pulled in moved %+v", r.Moved)
|
t.Errorf("a module no longer pulled in moved %+v", r.Moved)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1126 (ADR 0549 §3, §5): a module the catalogue's placement names on a node resolves there as an assigned one does,
|
|
||||||
// while the two sources stand; it goes when neither names it.
|
|
||||||
m.mirror(t, PlacementRead, "placement.ace", `{"v":1,"node":"ace","modules":[{"module":"dunst"}]}`)
|
|
||||||
if r, err := c.Reconcile(); err != nil || len(r.Moved) != 4 {
|
|
||||||
t.Fatalf("a placed module's values moved %+v (%v)", r.Moved, err)
|
|
||||||
}
|
|
||||||
expect(t, m, "ace", "10", SourceDefault)
|
|
||||||
_ = m.Delete(PlacementRead, "placement.ace")
|
|
||||||
if r, _ := c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
|
|
||||||
t.Errorf("a module no longer placed moved %+v", r.Moved)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user