Run the levels of one check in one ask after one fetch, keeping nothing under the candidate's runner or a digest without the siblings, as ADR 0525 §3 says a check ask is (issue 1887 A5a)

This commit is contained in:
2026-10-23 15:59:15 +02:00
parent 34338df950
commit ba092d543f
5 changed files with 141 additions and 61 deletions
+69 -33
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"path"
"slices"
"sort"
"strings"
@@ -162,19 +163,35 @@ func (s AskSteps) check(ctx context.Context, job delivery.CheckJob) (any, error)
if len(f.Conflicts) > 0 {
return answer, nil // the asker reads the conflict from the fetch's answer: a verdict, not an error
}
if s.Test == nil {
return nil, Refuse("this build module tests nothing")
}
runner := ""
if job.Runner != "" && (job.RunnerWhen == "" || changesUnder(f.Changed, job.RunnerWhen)) {
runner = job.Runner
}
for _, level := range job.Levels {
la := delivery.LevelAnswer{Level: level}
passed, err := s.level(ctx, job, level, f, runner, &la)
answer.Levels = append(answer.Levels, la)
if err != nil {
return nil, err
}
if !passed {
return answer, nil // the first level that did not pass ends the check (ADR 0484 clause 13)
}
}
return answer, nil
}
// level runs one level of a check ask and says whether it passed.
func (s AskSteps) level(ctx context.Context, job delivery.CheckJob, level string, f fetched, runner string, la *delivery.LevelAnswer) (bool, error) {
withRunner := func(args map[string]any) map[string]any {
if runner != "" {
args["runner"] = runner
}
return args
}
if s.Test == nil {
return nil, Refuse("this build module tests nothing")
}
test := func(args map[string]any) (bool, error) {
out, err := s.Test(ctx, args)
if err != nil {
@@ -184,7 +201,7 @@ func (s AskSteps) check(ctx context.Context, job delivery.CheckJob) (any, error)
if err != nil {
return false, err
}
answer.Tests = append(answer.Tests, r)
la.Tests = append(la.Tests, r)
var t struct {
Passed bool `json:"passed"`
}
@@ -198,23 +215,20 @@ func (s AskSteps) check(ctx context.Context, job delivery.CheckJob) (any, error)
if changesTestIn(f.Changed, dir) != priority {
continue
}
passed, err := test(withRunner(map[string]any{"workspace": f.Workspace, "module": dir, "level": job.Level}))
passed, err := test(withRunner(map[string]any{"workspace": f.Workspace, "module": dir, "level": level}))
if err != nil {
return nil, fmt.Errorf("the %s level of %s: %w", job.Level, dir, err)
return false, fmt.Errorf("the %s level of %s: %w", level, dir, err)
}
if !passed {
return answer, nil
return false, nil
}
}
}
return answer, nil
}
todo, all, err := s.plan(ctx, job, f, withRunner, &answer)
if err != nil {
return nil, err
return true, nil
}
todo, all := s.plan(ctx, job, level, f, runner, withRunner, la)
for _, priority := range []bool{true, false} {
args := withRunner(map[string]any{"workspace": f.Workspace, "level": job.Level})
args := withRunner(map[string]any{"workspace": f.Workspace, "level": level})
if job.Run != "" {
args["run"] = job.Run
}
@@ -236,13 +250,13 @@ func (s AskSteps) check(ctx context.Context, job delivery.CheckJob) (any, error)
}
passed, err := test(args)
if err != nil {
return nil, fmt.Errorf("the %s level: %w", job.Level, err)
return false, fmt.Errorf("the %s level: %w", level, err)
}
if !passed {
return answer, nil
return false, nil
}
}
return answer, nil
return true, nil
}
// release removes an ask's workspace, saying a failure: the module's sweep removes it later.
@@ -255,30 +269,36 @@ 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 (a
// try of some tests, or a digest that named nothing).
func (s AskSteps) plan(ctx context.Context, job delivery.CheckJob, f fetched, withRunner func(map[string]any) map[string]any, answer *delivery.CheckAnswer) ([]string, bool, error) {
if job.Run != "" {
return nil, true, nil // a partial run proves no package: nothing is taken or kept
// 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
}
out, err := s.Digest(ctx, withRunner(map[string]any{"workspace": f.Workspace, "level": job.Level}))
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", job.Level, err)
s.Logf("no digest of the %s level, so it runs whole: %v", level, err)
}
return nil, true, nil
return nil, true
}
r, err := raw(out)
if err != nil {
return nil, true, nil
return nil, true
}
answer.Digest = r
var d struct {
Packages map[string]string `json:"packages"`
Beside []string `json:"beside"`
}
if err := json.Unmarshal(r, &d); err != nil || len(d.Packages) == 0 {
return nil, true, nil
return nil, 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
}
la.Digest = r
pkgs := make([]string, 0, len(d.Packages))
for p := range d.Packages {
pkgs = append(pkgs, p)
@@ -287,21 +307,37 @@ func (s AskSteps) plan(ctx context.Context, job delivery.CheckJob, f fetched, wi
var todo []string
for _, p := range pkgs {
if job.Kept && s.Kept != nil {
tests, found, err := s.Kept(ctx, job.Level, d.Packages[p])
tests, found, err := s.Kept(ctx, level, d.Packages[p])
if err != nil && s.Logf != nil {
s.Logf("the kept verdict of %s at %s cannot be read, so it runs: %v", p, job.Level, err)
s.Logf("the kept verdict of %s at %s cannot be read, so it runs: %v", p, level, err)
}
if err == nil && found {
answer.Kept = append(answer.Kept, delivery.KeptTaken{Package: p, Digest: d.Packages[p], Tests: tests})
la.Kept = append(la.Kept, delivery.KeptTaken{Package: p, Digest: d.Packages[p], Tests: tests})
continue
}
}
todo = append(todo, p)
}
if len(todo) == 0 {
return nil, false, nil
return todo, false
}
// besideOf are the siblings a fetch's arguments place, as a digest echoes them: `<owner/repo> <commit>`, sorted.
func besideOf(fetch map[string]any) []string {
raw, err := json.Marshal(fetch["beside"])
if err != nil {
return nil
}
return todo, false, nil
var refs []struct {
Repository string `json:"repository"`
Commit string `json:"commit"`
}
_ = json.Unmarshal(raw, &refs)
var out []string
for _, r := range refs {
out = append(out, r.Repository+" "+r.Commit)
}
sort.Strings(out)
return out
}
// changesUnder says whether a path the merge changes is under prefix.