diff --git a/go/buildstep/askrun.go b/go/buildstep/askrun.go index 7f0ee11..c59d839 100644 --- a/go/buildstep/askrun.go +++ b/go/buildstep/askrun.go @@ -226,7 +226,10 @@ func (s AskSteps) level(ctx context.Context, job delivery.CheckJob, level string } return true, nil } - todo, all := s.plan(ctx, job, level, f, runner, withRunner, la) + todo, all, keepRunner := s.plan(ctx, job, level, f, runner, withRunner, la) + if !keepRunner { + runner = "" // an installed module that ignores the runner judges the level as it judges any other (ADR 0535 §2) + } for _, priority := range []bool{true, false} { args := withRunner(map[string]any{"workspace": f.Workspace, "level": level}) if job.Run != "" { @@ -269,34 +272,44 @@ func (s AskSteps) release(workspace string) { } } -// plan digests the level and takes the kept verdicts: the packages left to test, or all when the level runs whole. It -// runs whole, digesting nothing, for a try of some tests (a partial run proves no package) and under the candidate's own -// runner (ADR 0535 §4: the candidate decides which verdicts would be taken, so every package runs); it runs whole and -// takes nothing when the digest names no package or does not cover the siblings the fetch placed (ADR 0444, L5). -func (s AskSteps) plan(ctx context.Context, job delivery.CheckJob, level string, f fetched, runner string, withRunner func(map[string]any) map[string]any, la *delivery.LevelAnswer) ([]string, bool) { - if job.Run != "" || runner != "" { - return nil, true +// plan digests the level and takes the kept verdicts: the packages left to test, or all when the level runs whole, and +// whether the runner stays. A try of some tests runs whole, digesting nothing (a partial run proves no package). Under the +// candidate's runner the digest is asked as the candidate: when the candidate answers it, every package runs and nothing +// is digested for keeping (ADR 0535 §4); when the installed module answers it (it does not take the runner), the level is +// planned as any other and the runner is dropped. The level runs whole and takes nothing when the digest names no package +// or does not cover the siblings the fetch placed (ADR 0444, L5). +func (s AskSteps) plan(ctx context.Context, job delivery.CheckJob, level string, f fetched, runner string, withRunner func(map[string]any) map[string]any, la *delivery.LevelAnswer) ([]string, bool, bool) { + if job.Run != "" { + return nil, true, true } out, err := s.Digest(ctx, withRunner(map[string]any{"workspace": f.Workspace, "level": level})) if err != nil { if s.Logf != nil { s.Logf("no digest of the %s level, so it runs whole: %v", level, err) } - return nil, true + return nil, true, true } r, err := raw(out) if err != nil { - return nil, true + return nil, true, true } var d struct { Packages map[string]string `json:"packages"` Beside []string `json:"beside"` + JudgedBy string `json:"judged_by"` } - if err := json.Unmarshal(r, &d); err != nil || len(d.Packages) == 0 { - return nil, true + if err := json.Unmarshal(r, &d); err != nil { + return nil, true, true } - if !slices.Equal(d.Beside, besideOf(job.Fetch)) { - return nil, true // a digest that does not cover the siblings keys no verdict: the level runs whole, keeping nothing + if runner != "" { + if strings.HasPrefix(d.JudgedBy, "candidate ") { + la.Judged = d.JudgedBy + return nil, true, true // the candidate decides which verdicts would be taken: all run, nothing kept + } + runner = "" + } + if len(d.Packages) == 0 || !slices.Equal(d.Beside, besideOf(job.Fetch)) { + return nil, true, runner != "" // a digest that names nothing or not the siblings keys no verdict: the level runs whole } la.Digest = r pkgs := make([]string, 0, len(d.Packages)) @@ -318,7 +331,7 @@ func (s AskSteps) plan(ctx context.Context, job delivery.CheckJob, level string, } todo = append(todo, p) } - return todo, false + return todo, false, false } // besideOf are the siblings a fetch's arguments place, as a digest echoes them: ` `, sorted. diff --git a/go/buildstep/askrun_test.go b/go/buildstep/askrun_test.go index 473c2a1..ebc8f40 100644 --- a/go/buildstep/askrun_test.go +++ b/go/buildstep/askrun_test.go @@ -140,19 +140,40 @@ test {"level":"unit","packages":["x/pkg/b"],"workspace":"/w"}` } } -// ADR 0535 §4: a merge that changes the build module's own code is judged by the candidate's runner, every package run -// and nothing digested or kept; a digest that does not cover the fetch's siblings runs the level whole too. +// ADR 0535 §2, §4: a merge that changes the build module's own code is judged by the candidate's runner: its digest says +// the candidate ran, and every package runs with nothing kept; an installed module that ignores the runner is planned as +// any other, the runner dropped. A digest that does not cover the fetch's siblings runs the level whole too. func TestRunAskRunsWholeUnderTheRunnerOrWithoutTheSiblings(t *testing.T) { - r := &recorder{fetch: map[string]any{"workspace": "/w", "changed": []string{"modules/mesh-build-go/x.go"}}, - digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}} - if _, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true, - Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})); err != nil { - t.Fatal(err) + for _, tc := range []struct{ judged, want string }{ + {"candidate 999", `digest {"level":"unit","runner":"workspace","workspace":"/w"} +test {"level":"unit","runner":"workspace","workspace":"/w"}`}, + {"installed", `digest {"level":"unit","runner":"workspace","workspace":"/w"}`}, + } { + r := &recorder{fetch: map[string]any{"workspace": "/w", "changed": []string{"modules/mesh-build-go/x.go"}}, + digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}} + steps := r.steps() + digest := steps.Digest + steps.Digest = func(ctx context.Context, args map[string]any) (any, error) { + _, _ = digest(ctx, args) + return map[string]any{"level": "unit", "packages": r.digests, "judged_by": tc.judged}, nil + } + out, err := RunAsk(context.Background(), steps, ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true, + Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})) + if err != nil { + t.Fatal(err) + } + if got := strings.Join(r.calls[1:], "\n"); got != tc.want { + t.Errorf("judged by %s: calls\n%s\nwant\n%s", tc.judged, got, tc.want) + } + l := out.(delivery.CheckAnswer).Levels[0] + if tc.judged == "installed" && (len(l.Kept) != 1 || l.Judged != "") { + t.Errorf("an installed module's level %+v, want the kept verdict taken", l) + } + if tc.judged != "installed" && (len(l.Kept) != 0 || len(l.Digest) != 0 || l.Judged != tc.judged) { + t.Errorf("the candidate's level %+v, want nothing kept and the program named", l) + } } - if got := strings.Join(r.calls[1:], "\n"); got != `test {"level":"unit","runner":"workspace","workspace":"/w"}` { - t.Fatalf("under the runner: %s", got) - } - r = &recorder{fetch: map[string]any{"workspace": "/w"}, digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}} + r := &recorder{fetch: map[string]any{"workspace": "/w"}, digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}} out, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true, Fetch: map[string]any{"repository": "novox/x", "commit": runCommit, "beside": []any{map[string]any{"repository": "novox/mesh-sdk", "commit": runCommit}}}})) if err != nil { diff --git a/go/delivery/stepasks.go b/go/delivery/stepasks.go index 5cda634..a7ae9d2 100644 --- a/go/delivery/stepasks.go +++ b/go/delivery/stepasks.go @@ -346,7 +346,10 @@ type CheckAnswer struct { // LevelAnswer is one level of a check ask: the digest's answer (absent when nothing was digested: the level ran whole and // keeps nothing), the kept verdicts taken, and one test result per test step run, in the order run. type LevelAnswer struct { - Level string `json:"level"` + Level string `json:"level"` + // Judged is what the digest under the candidate's runner said of the program that ran it ("candidate "), + // when the candidate judged the level (ADR 0535 §4, §5); empty otherwise. + Judged string `json:"judged,omitempty"` Digest json.RawMessage `json:"digest,omitempty"` Kept []KeptTaken `json:"kept,omitempty"` Tests []json.RawMessage `json:"tests,omitempty"`