Judge a head merged onto its trunk through the build modules: run only packages without a kept verdict, the change's own tests first, stop at the first failure (issues 1679, 1288, 1289)

This commit is contained in:
2026-10-21 21:11:43 +02:00
parent 27072a01b5
commit cfbd5b6d2d
12 changed files with 1261 additions and 13 deletions
@@ -2,9 +2,11 @@ package main
import (
"context"
"errors"
"log"
"sort"
"sync"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/delivery/buildrun"
"mesh-delivery-checker/internal/check"
@@ -12,7 +14,6 @@ import (
// judgeDeps are the parts the judge of a check is made of.
type judgeDeps struct {
checker *check.Checker
runner buildrun.Runner
tools *buildrun.ToolchainSet
presence *buildrun.PresenceSet
@@ -21,15 +22,90 @@ type judgeDeps struct {
trunks *check.Trunks
kept check.KV
previews check.KV
repos *repoModules
logf func(format string, args ...any)
}
var _ = log.Printf
type unbuilt struct{}
func (unbuilt) Judge(context.Context, *check.Case) (check.Outcome, error) {
return check.Outcome{}, errors.New("the judge of a check is not built yet")
func newJudge(d judgeDeps) check.Judge {
return &check.Examiner{Steps: d.runner, Toolchains: d.tools, Presence: d.presence, Health: d.have, Places: d.places,
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Trunks: d.trunks, Logf: d.logf}
}
func newJudge(judgeDeps) check.Judge { return unbuilt{} }
// manifestKV reads a manifest by its digest.
type manifestKV interface {
Get(ctx context.Context, key string) (value []byte, revision uint64, found bool, err error)
}
// repoModules knows which build modules judge a repository, from the indexer's modules and their manifests: the Go build
// module always (a repository's go.mod files are its own), the Node one when a module of the repository has a TypeScript
// artifact (ADR 0484 clause 10). Safe for use from the watch and from a check at once.
type repoModules struct {
manifests manifestKV
logf func(format string, args ...any)
mu sync.Mutex
langs map[string]map[string]bool // by manifest digest: the languages of its artifacts
repoOf map[string]string // by module: its repository
typed map[string]string // by module: its manifest digest
}
// Put takes a change of the indexer's modules bucket in: a module's repository and the languages its manifest names.
func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted bool) {
r.mu.Lock()
if r.repoOf == nil {
r.repoOf, r.typed, r.langs = map[string]string{}, map[string]string{}, map[string]map[string]bool{}
}
if deleted {
delete(r.repoOf, key)
delete(r.typed, key)
r.mu.Unlock()
return
}
r.mu.Unlock()
m, err := delivery.DecodeModule(value)
if err != nil {
return
}
r.mu.Lock()
r.repoOf[key] = m.Repository
r.typed[key] = m.Manifest
_, known := r.langs[m.Manifest]
r.mu.Unlock()
if m.Manifest == "" || known {
return
}
set := map[string]bool{}
if mk, err := delivery.ManifestsKey(m.Manifest); err == nil {
if raw, _, found, err := r.manifests.Get(ctx, mk); err == nil && found {
if kept, err := delivery.DecodeManifest(raw); err == nil {
if man, err := catalogue.ParseManifest([]byte(kept.JSON)); err == nil && man.Build != nil {
for _, a := range man.Build.Artifacts {
if a.Language != "" {
set[a.Language] = true
}
}
} else if err != nil && r.logf != nil {
r.logf("the manifest %s cannot be read, so its module counts as no TypeScript: %v", m.Manifest, err)
}
}
}
}
r.mu.Lock()
r.langs[m.Manifest] = set
r.mu.Unlock()
}
// BuildModules are the build modules that judge a repository, sorted.
func (r *repoModules) BuildModules(repository string) []string {
out := []string{buildrun.GoBuildModule}
r.mu.Lock()
defer r.mu.Unlock()
for module, repo := range r.repoOf {
if repo == repository && r.langs[r.typed[module]]["typescript"] {
out = append(out, buildrun.NodeBuildModule)
break
}
}
sort.Strings(out)
return out
}
@@ -0,0 +1,72 @@
package main
import (
"context"
"encoding/json"
"os"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/delivery/buildrun"
)
type memManifests map[string][]byte
func (m memManifests) Get(_ context.Context, key string) ([]byte, uint64, bool, error) {
v, ok := m[key]
return v, 1, ok, nil
}
func putModule(t *testing.T, r *repoModules, name, repository, manifest string) {
t.Helper()
data, err := delivery.EncodeModule(delivery.Module{Repository: repository, Commit: strings.Repeat("a", 40), FP: "src1:" + strings.Repeat("a", 64), Manifest: delivery.ManifestDigest([]byte(manifest))})
if err != nil {
t.Fatal(err)
}
r.Put(context.Background(), name, data, false)
}
func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *testing.T) { // 1292/AC2
own, err := os.ReadFile("../../module.json") // a manifest with a Go bundle: this module's own
if err != nil {
t.Fatal(err)
}
goManifest := string(own)
// A TypeScript bundle names neither a system nor a directory it is built from (the catalogue's parser says so).
var doc map[string]any
if err := json.Unmarshal(own, &doc); err != nil {
t.Fatal(err)
}
artifacts := doc["build"].(map[string]any)["artifacts"].([]any)
program := artifacts[0].(map[string]any)
program["language"] = "typescript"
delete(program, "system")
delete(program, "from")
delete(program, "binary")
tsRaw, err := json.Marshal(doc)
if err != nil {
t.Fatal(err)
}
tsManifest := string(tsRaw)
store := memManifests{}
for _, m := range []string{goManifest, tsManifest} {
data, err := delivery.EncodeManifest(delivery.Manifest{Module: "demo", JSON: m})
if err != nil {
t.Fatal(err)
}
store[delivery.ManifestDigest([]byte(m))] = data
}
r := &repoModules{manifests: store}
putModule(t, r, "mesh-delivery-checker", "novox/mesh-delivery", goManifest)
putModule(t, r, "mesh-ui", "novox/mesh-catalog", tsManifest)
if got := r.BuildModules("novox/mesh-delivery"); strings.Join(got, ",") != buildrun.GoBuildModule {
t.Errorf("mesh-delivery: %v, want the Go build module alone", got)
}
if got := r.BuildModules("novox/mesh-catalog"); strings.Join(got, ",") != buildrun.GoBuildModule+","+buildrun.NodeBuildModule {
t.Errorf("mesh-catalog: %v, want both", got)
}
if got := r.BuildModules("novox/mesh-sdk"); strings.Join(got, ",") != buildrun.GoBuildModule {
t.Errorf("a repository with no module: %v, want the Go build module", got)
}
}
@@ -192,10 +192,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
Link: link,
Logf: logger.Printf,
}
repos := &repoModules{manifests: kvStore{buckets[delivery.ManifestsBucket]}, logf: logger.Printf}
ch.Judge = newJudge(judgeDeps{
checker: ch, runner: runner, tools: tools, presence: presence, have: have, places: places, trunks: trunks,
runner: runner, tools: tools, presence: presence, have: have, places: places, trunks: trunks,
kept: kvStore{buckets[delivery.CheckerKeptBucket]}, previews: kvStore{buckets[delivery.PreviewsBucket]},
logf: logger.Printf,
repos: repos, logf: logger.Printf,
})
// The toolchains first, so a check that starts from the first candidate finds the nodes.
@@ -220,7 +221,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
defer cc.Stop()
}
errs := make(chan error, 6)
errs := make(chan error, 8)
// The watchman's presence says which nodes are asleep. Where its bucket is not there yet no node is online, so nothing
// is checked; the checker says so and is started again with the watchman.
switch kv, err := js.KeyValue(ctx, delivery.PresenceBucket); {
@@ -285,6 +286,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
})
}()
}
// The modules say which repository holds a TypeScript module, so the Node build module judges it too.
mw, err := settle(ctx, buckets[delivery.ModulesBucket], func(e jetstream.KeyValueEntry) { repos.Put(ctx, e.Key(), e.Value(), !opOf(e)) })
if err != nil {
return err
}
go func() {
errs <- follow(ctx, mw, func(e jetstream.KeyValueEntry) { repos.Put(ctx, e.Key(), e.Value(), !opOf(e)) })
}()
each := func(e jetstream.KeyValueEntry) {
if opOf(e) {
ch.Candidate(ctx, e.Key(), e.Value())
@@ -0,0 +1,62 @@
package check
import "strconv"
// BesideRef is a sibling repository a repository's tests read, placed beside the fetched tree as a plain directory (ADR
// 0484 clause 4). Ref names the pull request's head when the commit is only there.
type BesideRef struct {
Repository string `json:"repository"`
Commit string `json:"commit"`
Ref string `json:"ref,omitempty"`
}
// besideRules are the repositories each core repository's tests read, as the tests state it (mesh-sdk go/internal/beside
// beside.go): the catalogue at its trunk, the node-engine, the SDK as the controller pins it, and the delivery's pinned
// forge dependencies (mesh-catalog checks/deps). The checker chooses which sibling at which commit; the build module only
// places what it is told.
var besideRules = map[string][]string{
"novox/mesh-controller": {"novox/mesh-catalog", "novox/mesh-host", "novox/mesh-sdk"},
"novox/mesh-host": {"novox/mesh-catalog", "novox/mesh-sdk"},
"novox/mesh-sdk": {"novox/mesh-catalog", "novox/mesh-host"},
"novox/mesh-catalog": {"novox/mesh-sdk", "novox/mesh-controller"},
"novox/mesh-delivery": {"novox/mesh-sdk", "novox/mesh-catalog"},
}
// Beside are the siblings a pull request's tests read: each at its trunk head, and at the head of the group's pull request
// when the group holds one of that repository (a group's members are checked together, ADR 0482 clause 2) — except the
// catalogue, which the tests read at its trunk even when the group holds a catalogue pull request (beside.go).
func (j *Examiner) Beside(c *Case, p Pull) []BesideRef {
var heads map[string]string
if j.Trunks != nil {
heads = j.Trunks.Heads()
}
var out []BesideRef
for _, repo := range besideRules[p.Repository] {
if member := c.member(repo); member != nil && repo != "novox/mesh-catalog" {
out = append(out, BesideRef{Repository: repo, Commit: member.Head, Ref: refOf(member.Number)})
continue
}
if head := heads[repo]; head != "" {
out = append(out, BesideRef{Repository: repo, Commit: head})
}
}
return out
}
func (c *Case) member(repository string) *Pull {
for i := range c.Pulls {
if c.Pulls[i].Repository == repository {
return &c.Pulls[i]
}
}
return nil
}
func refOf(number int) string {
if number <= 0 {
return ""
}
return "refs/pull/" + itoa(number) + "/head"
}
func itoa(n int) string { return strconv.Itoa(n) }
@@ -23,6 +23,8 @@ const NodesWords = "build node qualifies"
type Outcome struct {
Verdict delivery.Verdict
Builds []BuildNote
// Slowest names the slowest tests of a level that went over its bound ("unit pkg.Test 1.2s").
Slowest []string
}
// BuildNote is the state of a candidate's module build, by module: built, failed or waiting (ADR 0484 clause 7).
@@ -0,0 +1,456 @@
package check
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/delivery/buildrun"
)
// captured reads a result the build module's own code marshalled (testdata/captured/README).
func captured[T any](t testing.TB, name string) T {
t.Helper()
raw, err := os.ReadFile(filepath.Join("testdata", "captured", name+".json"))
if err != nil {
t.Fatal(err)
}
var v T
if err := json.Unmarshal(raw, &v); err != nil {
t.Fatal(err)
}
return v
}
type stepCall struct {
module, node, tool string
args map[string]any
}
// fakeSteps stands for the build modules: a handler for each tool answers what the module's code answers.
type fakeSteps struct {
mu sync.Mutex
calls []stepCall
handlers map[string]func(c stepCall) (any, error)
}
func (f *fakeSteps) Run(_ context.Context, module, node, tool string, args map[string]any) (json.RawMessage, error) {
c := stepCall{module, node, tool, args}
f.mu.Lock()
f.calls = append(f.calls, c)
h := f.handlers[tool]
f.mu.Unlock()
if h == nil {
return nil, fmt.Errorf("the fake build module has no tool %s", tool)
}
v, err := h(c)
if err != nil {
return nil, err
}
return json.Marshal(v)
}
func (f *fakeSteps) tool(name string) []stepCall {
f.mu.Lock()
defer f.mu.Unlock()
var out []stepCall
for _, c := range f.calls {
if strings.HasSuffix(c.tool, "_"+name) {
out = append(out, c)
}
}
return out
}
type entries []delivery.NodeToolchain
func (e entries) Entries(language string) []delivery.NodeToolchain {
var out []delivery.NodeToolchain
for _, x := range e {
if x.Language == language {
out = append(out, x)
}
}
return out
}
type langs map[string][]string
func (l langs) BuildModules(repository string) []string {
if m, ok := l[repository]; ok {
return m
}
return []string{buildrun.GoBuildModule}
}
func goEntries(nodes ...string) entries {
var e entries
for _, n := range nodes {
e = append(e, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "7"}})
e = append(e, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: "typescript", Version: "node24", Recipe: "3"}})
}
return e
}
const digC = "cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33"
const digD = "dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44"
type bench struct {
ex *Examiner
steps *fakeSteps
kept *memKV
prev *memKV
fetch Fetched
digest map[string]Digested // by level
test func(c stepCall) (any, error)
}
// newBench is an examiner over fake build modules that merge cleanly, digest the captured fixture's packages and pass
// the captured slow fixture's result at both levels, on shanks.
func newBench(t testing.TB) *bench {
b := &bench{steps: &fakeSteps{}, kept: newMemKV(), prev: newMemKV()}
b.fetch = Fetched{Workspace: "ws1", Commit: head1, Modules: []string{"."}, Base: trunkA, Merge: "9999999999999999999999999999999999999999",
Changed: []string{"README.md"}}
one := captured[Digested](t, "go-digest")
b.digest = map[string]Digested{
delivery.LevelUnit: {Level: "unit", Packages: map[string]string{"fixture/slow": digA}},
delivery.LevelIntegration: {Level: "integration", Packages: map[string]string{"fixture/slow": digB}},
}
_ = one
slow := captured[Tested](t, "go-test-slow")
b.test = func(c stepCall) (any, error) {
r := slow
r.Level, _ = c.args["level"].(string)
return r, nil
}
b.steps.handlers = map[string]func(stepCall) (any, error){
"build_go_fetch": func(stepCall) (any, error) { return b.fetch, nil },
"build_node_fetch": func(stepCall) (any, error) { return b.fetch, nil },
"build_go_digest": func(c stepCall) (any, error) { return b.digest[c.args["level"].(string)], nil },
"build_go_test": func(c stepCall) (any, error) { return b.test(c) },
"build_node_test": func(c stepCall) (any, error) { return b.test(c) },
}
now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
b.ex = &Examiner{Steps: b.steps, Toolchains: goEntries("shanks"), Places: &buildrun.Places{}, Kept: Kept{Store: b.kept}, Previews: b.prev,
Modules: langs{}, Trunks: &Trunks{}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }}
return b
}
func caseOf(t testing.TB, pulls ...delivery.CandidatePull) *Case {
t.Helper()
raw := candidateOf(t, "feat/x", pulls...)
c, err := delivery.DecodeCandidate(raw)
if err != nil {
t.Fatal(err)
}
cs := CaseOf(c, map[string]string{repoA: trunkA, repoSDK: trunkS})
if cs == nil {
t.Fatal("no case")
}
return cs
}
func (b *bench) judge(t testing.TB, cs *Case) Outcome {
t.Helper()
out, err := b.ex.Judge(context.Background(), cs)
if err != nil {
t.Fatal(err)
}
return out
}
func testedWith(t testing.TB, name string, edit func(*Tested)) Tested {
r := captured[Tested](t, name)
if edit != nil {
edit(&r)
}
return r
}
func packagesOf(c stepCall) []string {
var out []string
if l, ok := c.args["packages"].([]string); ok {
out = l
}
return out
}
func TestACleanMergeIsFetchedAtItsTrunkAndBothLevelsPass(t *testing.T) { // 1679/AC1
b := newBench(t)
cs := caseOf(t, pullOf(repoA, 7, head1))
out := b.judge(t, cs)
v := out.Verdict
if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 2 || v.Levels[delivery.LevelIntegration].Run != 2 {
t.Fatalf("verdict = %+v", v)
}
if v.Composition == nil || v.Composition.State != delivery.CompositionNotRun {
t.Errorf("composition = %+v, want not run", v.Composition)
}
if len(v.Machines) != 1 || v.Machines[0] != "shanks" {
t.Errorf("machines = %v", v.Machines)
}
fetch := b.steps.tool("fetch")
if len(fetch) != 1 || fetch[0].args["repository"] != repoA || fetch[0].args["commit"] != head1 || fetch[0].args["base"] != trunkA || fetch[0].args["ref"] != "refs/pull/7/head" {
t.Fatalf("fetch calls = %+v", fetch)
}
if v.Took.Clone <= 0 || v.Took.Unit <= 0 {
t.Errorf("took = %+v, want the clone and the unit level timed", v.Took)
}
// What passed is kept under each level's digest.
for _, k := range []string{"unit." + digA, "integration." + digB} {
if _, _, found, _ := b.kept.Get(context.Background(), k); !found {
t.Errorf("%s was not kept", k)
}
}
}
func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
b := newBench(t)
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/slow": digA, "fixture/other": digC}}
if _, err := (Kept{Store: b.kept}).Keep(context.Background(), "unit", digC, keptValue("fixture/other", 40)); err != nil {
t.Fatal(err)
}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
tests := b.steps.tool("test")
if len(tests) != 2 || strings.Join(packagesOf(tests[0]), ",") != "fixture/slow" || tests[0].args["level"] != "unit" {
t.Fatalf("test calls = %+v, want the unit level of fixture/slow alone, then the integration level", tests)
}
if u := out.Verdict.Levels[delivery.LevelUnit]; u.Run != 2 || u.Kept != 40 {
t.Errorf("unit = %+v, want 2 run and 40 kept", u)
}
}
func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3
b := newBench(t)
failing := captured[Tested](t, "go-test-failing")
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digB}}
b.test = func(c stepCall) (any, error) {
if c.args["level"] == "unit" {
return failing, nil
}
return captured[Tested](t, "go-test-slow"), nil
}
cs := caseOf(t, pullOf(repoA, 7, head1))
first := b.judge(t, cs).Verdict
if first.State != delivery.VerdictFail || first.Failed.Test != "unit: fixture/failing/bad.TestThreeIsFour" {
t.Fatalf("first = %+v", first)
}
if _, _, found, _ := b.kept.Get(context.Background(), "unit."+digB); !found {
t.Fatal("the package that passed in the failing run was not kept")
}
if _, _, found, _ := b.kept.Get(context.Background(), "unit."+digA); found {
t.Fatal("the package that failed was kept")
}
// Asked again with nothing changed: only the failed package runs; the good one is taken.
b.steps.calls = nil
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
tests := b.steps.tool("test")
if len(tests) == 0 || strings.Join(packagesOf(tests[0]), ",") != "fixture/failing/bad" {
t.Fatalf("the re-ask ran %+v, want only fixture/failing/bad", tests)
}
// The trunk moved and one digest changed: that package runs, the unchanged one is taken.
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digD}}
b.steps.calls = nil
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
tests = b.steps.tool("test")
if got := strings.Join(packagesOf(tests[0]), ","); got != "fixture/failing/bad,fixture/failing/good" && got != "fixture/failing/good,fixture/failing/bad" {
t.Errorf("after a digest changed the run was %q, want the failed package and the changed one", got)
}
}
func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1292/AC1
b := newBench(t)
b.ex.Modules = langs{repoA: {buildrun.NodeBuildModule}}
b.fetch.Modules = []string{"modules/ui"}
b.test = func(c stepCall) (any, error) {
return Tested{Level: c.args["level"].(string), Module: "modules/ui", Passed: true, Elapsed: 0.4, Bound: 5}, nil
}
cs := caseOf(t, pullOf(repoA, 7, head1))
for i := 0; i < 2; i++ {
v := b.judge(t, cs).Verdict
if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Kept != 0 || v.Levels[delivery.LevelUnit].Run != 1 {
t.Fatalf("round %d: verdict = %+v", i, v)
}
}
if n := len(b.steps.tool("test")); n != 4 {
t.Errorf("%d node test calls in two checks, want 4 (both levels each time)", n)
}
if n := len(b.steps.tool("digest")); n != 0 {
t.Errorf("the node build module was asked for a digest %d times", n)
}
if keys, _ := b.kept.Keys(context.Background()); len(keys) != 0 {
t.Errorf("kept %v for a node package", keys)
}
}
func TestTheChangesOwnFailingTestRunsFirstAndNothingElseRuns(t *testing.T) { // 1289/AC1, AC2
b := newBench(t)
b.fetch.Changed = []string{"modules/m/internal/b/b_test.go"}
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
failing := captured[Tested](t, "go-test-failing")
b.test = func(c stepCall) (any, error) {
if got := packagesOf(c); len(got) == 1 && got[0] == "m/internal/b" {
return failing, nil
}
return captured[Tested](t, "go-test-slow"), nil
}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if out.Verdict.State != delivery.VerdictFail || !strings.Contains(out.Verdict.Failed.Test, "TestThreeIsFour") {
t.Fatalf("verdict = %+v", out.Verdict)
}
tests := b.steps.tool("test")
if len(tests) != 1 || strings.Join(packagesOf(tests[0]), ",") != "m/internal/b" {
t.Fatalf("test calls = %+v, want only m/internal/b, once", tests)
}
if n := len(b.steps.tool("digest")); n != 1 {
t.Errorf("%d digest calls, want the unit level's alone (the check stopped)", n)
}
if out.Verdict.Composition == nil || out.Verdict.Composition.State != delivery.CompositionNotRun {
t.Errorf("composition = %+v", out.Verdict.Composition)
}
}
func TestTheChangesOwnPassingTestRunsFirstAndTheRestFollow(t *testing.T) { // 1289/AC3
b := newBench(t)
b.fetch.Changed = []string{"modules/m/internal/b/b_test.go", "modules/m/internal/a/a.go"}
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if out.Verdict.State != delivery.VerdictPass {
t.Fatalf("verdict = %+v", out.Verdict)
}
var unit []string
for _, c := range b.steps.tool("test") {
if c.args["level"] == "unit" {
unit = append(unit, strings.Join(packagesOf(c), ","))
}
}
if strings.Join(unit, " | ") != "m/internal/b | m/internal/a" {
t.Errorf("the unit level ran %v, want the changed test's package first, then the rest", unit)
}
}
func TestAHeadThatDoesNotMergeEndsFailedNamingTheFilesAndRunsNoTest(t *testing.T) { // 1289/AC4
b := newBench(t)
b.fetch = Fetched{Workspace: "ws1", Commit: head1, Base: trunkA, Conflicts: []string{"go.mod", "internal/x.go"}}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
f := out.Verdict.Failed
if out.Verdict.State != delivery.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "files in conflict: go.mod, internal/x.go") {
t.Fatalf("verdict = %+v", out.Verdict)
}
if n := len(b.steps.tool("digest")) + len(b.steps.tool("test")); n != 0 {
t.Errorf("%d digest and test steps ran after a conflict", n)
}
}
func TestNoQualifyingNodeIsAFailureOfTheNodes(t *testing.T) { // N6/AC4
b := newBench(t)
b.ex.Toolchains = entries{}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if !IsNodesFailure(out.Verdict) {
t.Fatalf("verdict = %+v, want a failure of the nodes", out.Verdict)
}
b.ex.Toolchains = goEntries("shanks")
b.ex.Presence = offline{}
if out = b.judge(t, caseOf(t, pullOf(repoA, 7, head1))); !IsNodesFailure(out.Verdict) {
t.Fatalf("an offline node: verdict = %+v", out.Verdict)
}
}
type offline struct{}
func (offline) Online(string) bool { return false }
func TestAStepThatFailsIsAVerdictNamingTheNode(t *testing.T) {
b := newBench(t)
b.steps.handlers["build_go_fetch"] = func(stepCall) (any, error) { return nil, fmt.Errorf("the step failed: ssh refused") }
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if f := out.Verdict.Failed; out.Verdict.State != delivery.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "ssh refused") {
t.Fatalf("verdict = %+v", out.Verdict)
}
}
func TestALevelWhoseDigestIsNotNamedRunsWholeAndKeepsNothing(t *testing.T) { // 1287/AC4
b := newBench(t)
b.steps.handlers["build_go_digest"] = func(stepCall) (any, error) { return nil, fmt.Errorf("go list failed") }
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if out.Verdict.State != delivery.VerdictPass {
t.Fatalf("verdict = %+v", out.Verdict)
}
tests := b.steps.tool("test")
if len(tests) != 2 || tests[0].args["packages"] != nil {
t.Errorf("test calls = %+v, want each level once, whole", tests)
}
if keys, _ := b.kept.Keys(context.Background()); len(keys) != 0 {
t.Errorf("kept %v with no digest", keys)
}
}
func TestALevelOverItsBoundIsNamedWithItsSlowestTestsAndKeepsItsVerdict(t *testing.T) { // 1300/AC3
b := newBench(t)
b.test = func(c stepCall) (any, error) {
return testedWith(t, "go-test-slow", func(r *Tested) {
r.Level = c.args["level"].(string)
r.OverBound = true
r.OverWhich = "run"
r.Elapsed = 7
}), nil
}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
v := out.Verdict
if v.State != delivery.VerdictPass || strings.Join(v.OverBound, ",") != "unit,integration" {
t.Fatalf("verdict = %+v", v)
}
if len(out.Slowest) == 0 || !strings.Contains(out.Slowest[0], "TestTakesAWhile") {
t.Errorf("slowest = %v", out.Slowest)
}
}
func TestSkippedTestsAreCountedPerLevel(t *testing.T) { // 1300/AC6, 1251
b := newBench(t)
b.test = func(c stepCall) (any, error) {
return testedWith(t, "go-test-slow", func(r *Tested) { r.Level = c.args["level"].(string); r.Skipped = 3 }), nil
}
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
if v.Levels[delivery.LevelUnit].Skipped != 3 || v.Levels[delivery.LevelIntegration].Skipped != 3 {
t.Errorf("levels = %+v", v.Levels)
}
}
func TestAGroupIsOneVerdictAndEachMembersTestsReadTheOthersAtTheirHeads(t *testing.T) { // 1624/AC1
b := newBench(t)
cs := caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))
v := b.judge(t, cs).Verdict
if v.State != delivery.VerdictPass {
t.Fatalf("verdict = %+v", v)
}
fetch := b.steps.tool("fetch")
if len(fetch) != 2 {
t.Fatalf("%d fetches, want one for each member", len(fetch))
}
var forA []BesideRef
for _, f := range fetch {
if f.args["repository"] == repoA {
forA, _ = f.args["beside"].([]BesideRef)
}
}
found := false
for _, r := range forA {
if r.Repository == repoSDK && r.Commit == head2 && r.Ref == "refs/pull/12/head" {
found = true
}
}
if !found {
t.Errorf("mesh-delivery's tests were not given the SDK member at its head: %+v", forA)
}
}
func TestAnExaminerIsAJudgeOfTheChecker(t *testing.T) {
var _ Judge = (*Examiner)(nil)
}
@@ -0,0 +1,455 @@
package check
import (
"context"
"encoding/json"
"fmt"
"path"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/delivery/buildrun"
)
// Steps runs a build module's long step on a node and answers its result (buildrun.Runner).
type Steps interface {
Run(ctx context.Context, module, node, tool string, args map[string]any) (json.RawMessage, error)
}
// ToolchainEntries are the nodes' toolchain entries of a language (buildrun.ToolchainSet).
type ToolchainEntries interface {
Entries(language string) []delivery.NodeToolchain
}
// BuildModules says which build modules judge a repository: the Go one always, the Node one when the repository holds a
// TypeScript module.
type BuildModules interface {
BuildModules(repository string) []string
}
// toolPrefix is the start of each build module's tool names.
var toolPrefix = map[string]string{buildrun.GoBuildModule: "build_go_", buildrun.NodeBuildModule: "build_node_"}
// Examiner judges a case through the build modules (ADR 0484 clauses 3, 4, 10, 13): for each pull request it fetches the head
// merged onto its trunk head on a build node, then runs the unit level and the integration level, taking the kept verdicts
// of unchanged packages, running the change's own tests first and stopping at the first failure.
type Examiner struct {
Steps Steps
Toolchains ToolchainEntries
// Presence says which nodes are online; Health whether a node's build module is running and healthy. A node that is not
// is not chosen; nil means every node is.
Presence interface{ Online(node string) bool }
Health interface {
Healthy(node, module string) bool
}
// Places holds a step back while its node is at the checker's capacity (ADR 0484 clause 12).
Places *buildrun.Places
Kept Kept
// Previews is the previews bucket; nil writes none.
Previews KV
Modules BuildModules
// Trunks are the trunk heads of every repository, for the siblings a repository's tests read.
Trunks interface{ Heads() map[string]string }
Now func() time.Time
Logf func(format string, args ...any)
}
func (j *Examiner) now() time.Time {
if j.Now != nil {
return j.Now()
}
return time.Now()
}
func (j *Examiner) logf(format string, args ...any) {
if j.Logf != nil {
j.Logf(format, args...)
}
}
// target is a pull request on one build module: the node chosen, the workspace its fetch made and what it said.
type target struct {
pull Pull
module string
node string
toolchain string
fetched Fetched
// priority are the Go packages (import paths) or the Node module directories whose test files the merge changes.
changed []string
plans map[string]*goPlan // by level
}
// goPlan is what a Go target has to run at one level: the digests, the packages without a kept verdict and the tests taken.
type goPlan struct {
digests map[string]string
todo []string
// all says the digest step gave nothing to key a kept verdict by, so the level runs whole and keeps nothing.
all bool
}
// acc collects a level's counts across targets.
type acc struct {
run, kept, skipped int
elapsed, setup float64
over bool
slowest []TestTime
}
// state is one judging.
type state struct {
j *Examiner
c *Case
targets []*target
levels map[string]*acc
nodes map[string]bool
clone time.Duration
slow []string
}
// Judge runs the case (it is the Judge of a Checker). A failure of the change, of a merge or of a step is a verdict; an error is only a case that cannot
// be judged at all.
func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) {
st := &state{j: j, c: c, levels: map[string]*acc{delivery.LevelUnit: {}, delivery.LevelIntegration: {}}, nodes: map[string]bool{}}
if v := st.fetchAll(ctx); v != nil {
return st.outcome(*v), nil
}
for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} {
for _, priority := range []bool{true, false} {
for _, t := range st.targets {
if v := st.runTarget(ctx, t, level, priority); v != nil {
return st.outcome(*v), nil
}
}
}
}
return st.outcome(delivery.Verdict{State: delivery.VerdictPass}), nil
}
// outcome fills the verdict's levels, times, machines and composition from what ran.
func (st *state) outcome(v delivery.Verdict) Outcome {
if v.State == delivery.VerdictPass || len(v.Levels) == 0 {
v.Levels = map[string]delivery.LevelResult{}
for level, a := range st.levels {
v.Levels[level] = delivery.LevelResult{Run: a.run, Kept: a.kept, Skipped: a.skipped}
}
}
ms := func(s float64) int64 { return int64(s * 1000) }
u, i := st.levels[delivery.LevelUnit], st.levels[delivery.LevelIntegration]
v.Took = delivery.Took{Clone: st.clone.Milliseconds(), Unit: ms(u.elapsed), IntegrationSetup: ms(i.setup), IntegrationRun: ms(i.elapsed)}
for node := range st.nodes {
v.Machines = append(v.Machines, node)
}
sort.Strings(v.Machines)
for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} {
if st.levels[level].over {
v.OverBound = append(v.OverBound, level)
}
}
// The composition is the assigner's check (issue 1297): until it delivers a verdict says it was not run (ADR 0484 clause 6).
v.Composition = &delivery.Composition{State: delivery.CompositionNotRun}
var slow []string
for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} {
a := st.levels[level]
if !a.over {
continue
}
sort.SliceStable(a.slowest, func(x, y int) bool { return a.slowest[x].Elapsed > a.slowest[y].Elapsed })
for n, s := range a.slowest {
if n == 3 {
break
}
slow = append(slow, fmt.Sprintf("%s %s %.1fs", level, path.Base(s.Package)+"."+s.Test, s.Elapsed))
}
}
return Outcome{Verdict: v, Slowest: slow}
}
// fail is a verdict of failure at a machine.
func fail(machine, words string) *delivery.Verdict {
v := failure(machine, words)
return &v
}
// step runs one step of a build module on the node the target is on, holding a place there while it runs.
func (st *state) step(ctx context.Context, t *target, tool, label string, args map[string]any) (json.RawMessage, error) {
st.c.Progress.Set(label, t.node)
release, err := st.j.Places.Acquire(ctx, t.node)
if err != nil {
return nil, err
}
defer release()
return st.j.Steps.Run(ctx, t.module, t.node, toolPrefix[t.module]+tool, args)
}
// choose picks the build node for a pull request's repository on a build module.
func (st *state) choose(bm string) (node, toolchain string, err error) {
language := buildrun.LanguageOf[bm]
qualifies := func(node string) bool {
return (st.j.Presence == nil || st.j.Presence.Online(node)) && (st.j.Health == nil || st.j.Health.Healthy(node, bm))
}
nodes, tc, err := buildrun.ChooseNodes(language, st.j.Toolchains.Entries(language), st.j.Places, qualifies, 1)
if err != nil {
return "", "", err
}
return nodes[0], tc.Version, nil
}
// fetchAll fetches every pull request's head merged onto its trunk on a build node of each build module the repository
// needs. A head that does not merge, or a step that fails, ends the check at once, running no test (ADR 0484 clause 13).
func (st *state) fetchAll(ctx context.Context) *delivery.Verdict {
began := st.j.now()
defer func() { st.clone = st.j.now().Sub(began) }()
for _, p := range st.c.Pulls {
for _, bm := range st.j.Modules.BuildModules(p.Repository) {
node, tc, err := st.choose(bm)
if err != nil {
return fail(bm, err.Error())
}
t := &target{pull: p, module: bm, node: node, toolchain: tc, plans: map[string]*goPlan{}}
st.nodes[node] = true
args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk}
if bm == buildrun.GoBuildModule {
if beside := st.j.Beside(st.c, p); len(beside) > 0 {
args["beside"] = beside
}
}
raw, err := st.step(ctx, t, "fetch", fmt.Sprintf("fetch %s#%d on %s", p.Repository, p.Number, node), args)
if err != nil {
return fail(node, fmt.Sprintf("fetch of %s#%d: %v", p.Repository, p.Number, err))
}
if err := json.Unmarshal(raw, &t.fetched); err != nil || t.fetched.Workspace == "" {
return fail(node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, raw))
}
if len(t.fetched.Conflicts) > 0 {
return fail(node, fmt.Sprintf("%s#%d does not merge into the trunk: files in conflict: %s", p.Repository, p.Number, strings.Join(t.fetched.Conflicts, ", ")))
}
t.changed = t.fetched.Changed
st.targets = append(st.targets, t)
}
}
return nil
}
// isTestFile says whether a repository-relative path is a test file of the languages the checker judges: Go's _test.go and
// the TypeScript module's .test. and .spec. files and test directories.
func isTestFile(p string) bool {
switch {
case strings.HasSuffix(p, "_test.go"):
return true
case strings.Contains(p, ".test.") || strings.Contains(p, ".spec."):
return true
}
for _, seg := range strings.Split(path.Dir(p), "/") {
if seg == "__tests__" || seg == "test" || seg == "tests" {
return true
}
}
return false
}
// changesTestOf says whether the merge changes a test file of the Go package with the import path pkg: the directory of the
// file and the import path agree on their last segments, whichever holds the module's directory and path in front.
func changesTestOf(changed []string, pkg string) bool {
for _, f := range changed {
if !strings.HasSuffix(f, "_test.go") {
continue
}
dir := path.Dir(f)
if dir == pkg || strings.HasSuffix(pkg, "/"+dir) || strings.HasSuffix(dir, "/"+pkg) {
return true
}
}
return false
}
// changesTestIn says whether the merge changes a test file inside the directory of a Node module.
func changesTestIn(changed []string, dir string) bool {
for _, f := range changed {
if !isTestFile(f) || strings.HasSuffix(f, "_test.go") {
continue
}
if dir == "." || dir == "" || strings.HasPrefix(f, strings.TrimSuffix(dir, "/")+"/") {
return true
}
}
return false
}
// failedName names a failure: its package and test.
func failedName(f Failure) string {
if f.Test == "" {
return f.Package
}
return f.Package + "." + f.Test
}
// firstLine is the first non-empty line of a failure's output, shortened.
func firstLine(s string) string {
for _, l := range strings.Split(s, "\n") {
if l = strings.TrimSpace(l); l != "" {
if len(l) > 120 {
l = l[:120]
}
return l
}
}
return ""
}
// failure turns a level that did not pass into a verdict naming what failed first.
func levelFailure(t *target, level string, res Tested) *delivery.Verdict {
v := delivery.Verdict{State: delivery.VerdictFail}
switch {
case len(res.Failed) > 0:
f := res.Failed[0]
name := failedName(f)
if t.module == buildrun.NodeBuildModule && f.Package == "" {
name = res.Module
}
v.Failed = delivery.FailedFirst{Test: level + ": " + name, Words: firstLine(f.Output)}
default:
why := res.Why
if why == "" {
why = "the " + level + " level did not pass"
}
v.Failed = delivery.FailedFirst{Test: level + ": " + t.pull.Repository, Words: why}
}
return &v
}
// runTarget runs one level of a target: first the packages the change's own tests are in (priority), then the others.
func (st *state) runTarget(ctx context.Context, t *target, level string, priority bool) *delivery.Verdict {
if t.module == buildrun.NodeBuildModule {
return st.runNode(ctx, t, level, priority)
}
return st.runGo(ctx, t, level, priority)
}
// A level's work is planned once for a target: the digest of each package, the kept verdicts taken, the packages left to run.
func (st *state) planGo(ctx context.Context, t *target, level string) *delivery.Verdict {
if t.plans[level] != nil {
return nil
}
plan := &goPlan{digests: map[string]string{}}
t.plans[level] = plan
raw, err := st.step(ctx, t, "digest", fmt.Sprintf("digest the %s level of %s#%d on %s", level, t.pull.Repository, t.pull.Number, t.node),
map[string]any{"workspace": t.fetched.Workspace, "level": level})
var d Digested
if err == nil {
err = json.Unmarshal(raw, &d)
}
if ctx.Err() != nil {
return fail(t.node, ctx.Err().Error())
}
if err != nil || len(d.Packages) == 0 {
// The build module named no digest (a level it cannot digest here): the level runs whole and keeps nothing.
if err != nil {
st.j.logf("%s#%d: no digest of the %s level, so it runs whole: %v", t.pull.Repository, t.pull.Number, level, err)
}
plan.all = true
return nil
}
plan.digests = d.Packages
a := st.levels[level]
pkgs := make([]string, 0, len(d.Packages))
for p := range d.Packages {
pkgs = append(pkgs, p)
}
sort.Strings(pkgs)
for _, p := range pkgs {
k, found, err := st.j.Kept.Lookup(ctx, level, d.Packages[p])
if err != nil {
st.j.logf("the kept verdict of %s at %s cannot be read, so it runs: %v", p, level, err)
}
if found {
a.kept += k.Tests
continue
}
plan.todo = append(plan.todo, p)
}
return nil
}
func (st *state) runGo(ctx context.Context, t *target, level string, priority bool) *delivery.Verdict {
if v := st.planGo(ctx, t, level); v != nil {
return v
}
plan := t.plans[level]
var run []string
switch {
case plan.all:
if priority {
return nil // nothing to put first: the level runs whole after
}
default:
for _, p := range plan.todo {
if changesTestOf(t.changed, p) == priority {
run = append(run, p)
}
}
if len(run) == 0 {
return nil
}
}
args := map[string]any{"workspace": t.fetched.Workspace, "level": level}
if run != nil {
args["packages"] = run
}
raw, err := st.step(ctx, t, "test", fmt.Sprintf("test the %s level of %s#%d on %s", level, t.pull.Repository, t.pull.Number, t.node), args)
if err != nil {
return fail(t.node, fmt.Sprintf("the %s level of %s#%d: %v", level, t.pull.Repository, t.pull.Number, err))
}
var res Tested
if err := json.Unmarshal(raw, &res); err != nil {
return fail(t.node, fmt.Sprintf("the %s level of %s#%d answered what is not a result: %v", level, t.pull.Repository, t.pull.Number, err))
}
a := st.levels[level]
a.run += res.Tests
a.skipped += res.Skipped
a.elapsed += res.Elapsed
a.setup += res.Setup
a.over = a.over || res.OverBound
a.slowest = append(a.slowest, res.Slowest...)
if len(plan.digests) > 0 {
if _, err := st.j.Kept.KeepPasses(ctx, t.pull.Repository, level, t.toolchain, res, plan.digests, st.j.now()); err != nil {
st.j.logf("%s#%d: a passing package could not be kept: %v", t.pull.Repository, t.pull.Number, err)
}
}
if !res.Passed {
return levelFailure(t, level, res)
}
return nil
}
func (st *state) runNode(ctx context.Context, t *target, level string, priority bool) *delivery.Verdict {
for _, dir := range t.fetched.Modules {
if changesTestIn(t.changed, dir) != priority {
continue
}
raw, err := st.step(ctx, t, "test", fmt.Sprintf("test the %s level of %s#%d (%s) on %s", level, t.pull.Repository, t.pull.Number, dir, t.node),
map[string]any{"workspace": t.fetched.Workspace, "module": dir, "level": level})
if err != nil {
return fail(t.node, fmt.Sprintf("the %s level of %s#%d (%s): %v", level, t.pull.Repository, t.pull.Number, dir, err))
}
var res Tested
if err := json.Unmarshal(raw, &res); err != nil {
return fail(t.node, fmt.Sprintf("the %s level of %s#%d (%s) answered what is not a result: %v", level, t.pull.Repository, t.pull.Number, dir, err))
}
if res.Module == "" {
res.Module = dir
}
a := st.levels[level]
if !res.NoTests {
a.run++ // a Node package is counted whole: its build module counts no tests (ADR 0484 clause 10)
}
a.elapsed += res.Elapsed
a.setup += res.Setup
a.over = a.over || res.OverBound
if !res.Passed {
return levelFailure(t, level, res)
}
}
return nil
}
@@ -0,0 +1,9 @@
go-test-failing.json, go-test-slow.json, go-test-empty.json and go-digest.json are what mesh-catalog's mesh-build-go answers
(TestResult and DigestResult, cmd/mesh-build-go levels.go and digest.go), marshalled by the module's own code: a throwaway
test beside the module's test_test.go and digest_test.go (workspaceFor + runUnit on the fixtures testdata/captured/failing,
slow and empty, and digestFixture + digests on testdata/repos/digest) at mesh-catalog origin/main cdbdb474, captured on
2026-10-21 for issue 1679 and not kept there. The fixtures' package names are the build module's own (fixture/failing/bad...).
The fields a fetch with a base adds (base, merge, conflicts, changed, trees) are the contract the lead relayed to the
mesh-catalog agent (ADR 0484 clause 3, 2026-10-21); mesh-catalog's fetch step is not on main yet, so the fetch fixtures in
the tests are written from that contract and are to be replaced by a capture once its fetch_test can marshal one.
@@ -0,0 +1,11 @@
{
"level": "unit",
"packages": {
"fixture/digest/a": "c288afe6252c38d9e7286ecb5f28b3c961780871fcc253670dcc3151c11c1df2",
"fixture/digest/c": "11abf5f32f0f12caafd76c565e4c2c2f3bf2e3ce6c2c7fcb6c2264a06db40087"
},
"without_tests": [
"fixture/digest/b",
"fixture/digest/d"
]
}
@@ -0,0 +1,21 @@
{
"level": "unit",
"passed": true,
"why": "the repository has no tests at the unit level",
"no_tests": true,
"tests": 0,
"skipped": 0,
"packages": [
{
"dir": ".",
"package": "fixture/empty",
"passed": true,
"elapsed": 0,
"tests": 0,
"no_tests": true
}
],
"elapsed": 0,
"bound": 5,
"over_bound": false
}
@@ -0,0 +1,45 @@
{
"level": "unit",
"passed": false,
"why": "1 failed: fixture/failing/bad TestThreeIsFour",
"tests": 2,
"skipped": 0,
"packages": [
{
"dir": ".",
"package": "fixture/failing/bad",
"passed": false,
"elapsed": 0.003,
"tests": 1
},
{
"dir": ".",
"package": "fixture/failing/good",
"passed": true,
"elapsed": 0.002,
"tests": 1
}
],
"elapsed": 0.003,
"bound": 5,
"slowest": [
{
"package": "fixture/failing/bad",
"test": "TestThreeIsFour",
"elapsed": 0
},
{
"package": "fixture/failing/good",
"test": "TestTwo",
"elapsed": 0
}
],
"failed": [
{
"package": "fixture/failing/bad",
"test": "TestThreeIsFour",
"output": " bad_test.go:7: Three() = 3, want 4\n--- FAIL: TestThreeIsFour (0.00s)"
}
],
"over_bound": false
}
@@ -0,0 +1,30 @@
{
"level": "unit",
"passed": true,
"tests": 2,
"skipped": 0,
"packages": [
{
"dir": ".",
"package": "fixture/slow",
"passed": true,
"elapsed": 0.065,
"tests": 2
}
],
"elapsed": 0.065,
"bound": 5,
"slowest": [
{
"package": "fixture/slow",
"test": "TestTakesAWhile",
"elapsed": 0.06
},
{
"package": "fixture/slow",
"test": "TestQuick",
"elapsed": 0
}
],
"over_bound": false
}