Compare commits
10
Commits
main
...
feat/try-speed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b763f8a2 | ||
|
|
50a2e4c4e2 | ||
|
|
4169faf3f0 | ||
|
|
103eec8855 | ||
|
|
8a62f23afe | ||
|
|
84395420ee | ||
|
|
9740842af7 | ||
|
|
588cfd54bb | ||
|
|
4ccd78c9ec | ||
|
|
6fe7b7dd6c |
@@ -3,7 +3,7 @@ module mesh-delivery-checker
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023213200-403a5d72ab93
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95
|
||||||
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,11 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023213200-403a5d72ab93 h1:FAiFY2KBKEoZSH49OZTt7/zRBIQxyl5g/KhYrpggbN4=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024214154-bc16740feb0d h1:oJREw0ggKqJWJ5DEISSL0i2ZC5XqdXAidxo9NJ74TCc=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023213200-403a5d72ab93/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024214154-bc16740feb0d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024231716-571ac521f053 h1:8Kbp3aJG3jGndDOfmGdMSDgJVEcc1hyu921hVnnlnlQ=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024231716-571ac521f053/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232250-9b408db24a58 h1:4Damtdqlf6u6nJhvMA99peDy1jmjixtcwFXJ4bWEkLk=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232250-9b408db24a58/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95 h1:I8zbgGzRjmgYydPTWo8hmaZW/P/g+qGDUfu3E32/pkU=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95/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=
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package check
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// countingAsks puts asks through inner, and answers no wait until want asks are put (or a bound passes, when it says
|
||||||
|
// how many were): a group whose asks are put one after another, each waited for, never gets past its first.
|
||||||
|
type countingAsks struct {
|
||||||
|
inner Asks
|
||||||
|
want int
|
||||||
|
mu sync.Mutex
|
||||||
|
puts int
|
||||||
|
all chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *countingAsks) Put(ctx context.Context, a delivery.StepAsk) error {
|
||||||
|
if err := c.inner.Put(ctx, a); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
c.puts++
|
||||||
|
if c.puts == c.want {
|
||||||
|
close(c.all)
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *countingAsks) Wait(ctx context.Context, a delivery.StepAsk) (delivery.Step, error) {
|
||||||
|
select {
|
||||||
|
case <-c.all:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return delivery.Step{}, fmt.Errorf("%d of %d asks were put before the first was waited for", c.puts, c.want)
|
||||||
|
}
|
||||||
|
return c.inner.Wait(ctx, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1921/S8: a group's asks are all put before any is waited for, so its members run side by side on the build nodes;
|
||||||
|
// the verdict is judged in member order as before.
|
||||||
|
func TestAGroupPutsEveryMembersAskBeforeTheFirstAnswer(t *testing.T) {
|
||||||
|
b := newBench(t)
|
||||||
|
counting := &countingAsks{inner: b.ex.Asks, want: 3, all: make(chan struct{})}
|
||||||
|
b.ex.Asks = counting
|
||||||
|
cs := caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2), pullOf("novox/mesh-controller", 3, head1))
|
||||||
|
v := b.judge(t, cs).Verdict
|
||||||
|
if v.State != delivery.VerdictPass {
|
||||||
|
t.Fatalf("verdict = %+v", v)
|
||||||
|
}
|
||||||
|
if counting.puts != 3 {
|
||||||
|
t.Errorf("%d asks were put, want 3", counting.puts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1921/S10: the levels of an ask run side by side, so what of the ask's time is its fetch (Took.Clone) is the ask's time
|
||||||
|
// less its longest level, not less the sum of its levels.
|
||||||
|
func TestTheCloneTimeTakesTheLongestLevelOffNotTheSum(t *testing.T) {
|
||||||
|
b := newBench(t)
|
||||||
|
b.test = func(c stepCall) (any, error) {
|
||||||
|
r := captured[Tested](t, "go-test-slow")
|
||||||
|
r.Level, _ = c.args["level"].(string)
|
||||||
|
r.Elapsed, r.Setup, r.Lint = 1.5, 0, 0 // the ask's step takes 2 s (bench.answer)
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
|
||||||
|
if v.State != delivery.VerdictPass || v.Took.Clone != 500 {
|
||||||
|
t.Errorf("clone = %d ms, want 500 (2 s less the longest level's 1.5 s): %+v", v.Took.Clone, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADR 0542 §2: a lint finding, which the Go build module marks as the lint step's, is reported as the lint step's failure,
|
||||||
|
// not as the unit level's.
|
||||||
|
func TestALintFindingIsTheLintStepsFailure(t *testing.T) {
|
||||||
|
b := newBench(t)
|
||||||
|
b.test = func(c stepCall) (any, error) {
|
||||||
|
r := captured[Tested](t, "go-test-slow")
|
||||||
|
r.Level, _ = c.args["level"].(string)
|
||||||
|
if r.Level == delivery.LevelUnit {
|
||||||
|
r.Passed = false
|
||||||
|
r.Failed = []Failure{{Package: ".", Test: "gofmt", Step: "lint", Output: "not as gofmt writes them (run gofmt -w): a.go"}}
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
|
||||||
|
if v.State != delivery.VerdictFail || v.Failed.Test != "lint: ..gofmt" {
|
||||||
|
t.Errorf("failed = %+v, want the lint step's failure", v.Failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,9 @@ func CaseOf(c delivery.Candidate, trunk map[string]string) *Case {
|
|||||||
}
|
}
|
||||||
cs := &Case{Key: key, Group: c.Group, HeadsHash: c.HeadsHash, Heads: GateHeads(c), Trunk: map[string]string{}, Progress: &Progress{}}
|
cs := &Case{Key: key, Group: c.Group, HeadsHash: c.HeadsHash, Heads: GateHeads(c), Trunk: map[string]string{}, Progress: &Progress{}}
|
||||||
for _, p := range c.Pulls {
|
for _, p := range c.Pulls {
|
||||||
|
if p.Merged {
|
||||||
|
continue // merged while the group waits for its other members: judged by the verdict the group had (issue 1791)
|
||||||
|
}
|
||||||
switch head, known := trunk[p.Repository]; {
|
switch head, known := trunk[p.Repository]; {
|
||||||
case !known || head == "":
|
case !known || head == "":
|
||||||
cs.Left = append(cs.Left, fmt.Sprintf("%s#%d: the trunk head of %s is not known", p.Repository, p.Number, p.Repository))
|
cs.Left = append(cs.Left, fmt.Sprintf("%s#%d: the trunk head of %s is not known", p.Repository, p.Number, p.Repository))
|
||||||
@@ -119,7 +122,18 @@ func GateHeads(c delivery.Candidate) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sameTrunk says whether two verdicts' trunk heads are the same.
|
// sameTrunk says whether two verdicts' trunk heads are the same.
|
||||||
func sameTrunk(a, b map[string]string) bool { return maps.Equal(a, b) }
|
// sameTrunk says whether a verdict judged on the trunk heads judged stands for a case on the trunk heads now: every
|
||||||
|
// repository of the case at the head it was judged on. A repository the verdict names and the case does not is a member
|
||||||
|
// that merged since (issue 1791): its own merge moved that trunk, and the members left are judged by the verdict the
|
||||||
|
// group had.
|
||||||
|
func sameTrunk(judged, now map[string]string) bool {
|
||||||
|
for repo, head := range now {
|
||||||
|
if judged[repo] != head {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(now) > 0 || len(judged) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// Trunks holds the trunk head of each repository, as the gate's source.main says (a watch puts it in).
|
// Trunks holds the trunk head of each repository, as the gate's source.main says (a watch puts it in).
|
||||||
type Trunks struct {
|
type Trunks struct {
|
||||||
|
|||||||
@@ -83,12 +83,24 @@ type Checker struct {
|
|||||||
tried map[string]bool // by verdict key: looked at since this process started
|
tried map[string]bool // by verdict key: looked at since this process started
|
||||||
epoch uint64 // changes of the nodes seen since this process started
|
epoch uint64 // changes of the nodes seen since this process started
|
||||||
nodesFail map[string]uint64 // by verdict key: the epoch a failure of the nodes was written at
|
nodesFail map[string]uint64 // by verdict key: the epoch a failure of the nodes was written at
|
||||||
|
// current is, by candidates key, the verdict key of the heads the group holds now; running holds, by candidates key,
|
||||||
|
// the stop of each check that runs, by its verdict key (issue 1791; issue 1921, try speed S9).
|
||||||
|
current map[string]string
|
||||||
|
running map[string]map[string]context.CancelCauseFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// superseded is why a check whose heads the group no longer holds was stopped: the heads it holds now.
|
||||||
|
type superseded struct{ by string }
|
||||||
|
|
||||||
|
func (s superseded) Error() string {
|
||||||
|
return "superseded: the group holds other heads now (" + s.by + "), whose check judges it"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Checker) init() {
|
func (ch *Checker) init() {
|
||||||
ch.once.Do(func() {
|
ch.once.Do(func() {
|
||||||
ch.known, ch.inflight, ch.again, ch.tried = map[string]delivery.Candidate{}, map[string]bool{}, map[string]bool{}, map[string]bool{}
|
ch.known, ch.inflight, ch.again, ch.tried = map[string]delivery.Candidate{}, map[string]bool{}, map[string]bool{}, map[string]bool{}
|
||||||
ch.nodesFail = map[string]uint64{}
|
ch.nodesFail = map[string]uint64{}
|
||||||
|
ch.current, ch.running = map[string]string{}, map[string]map[string]context.CancelCauseFunc{}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +138,15 @@ func (ch *Checker) Candidate(ctx context.Context, key string, value []byte) {
|
|||||||
}
|
}
|
||||||
ch.mu.Lock()
|
ch.mu.Lock()
|
||||||
ch.known[key] = c
|
ch.known[key] = c
|
||||||
|
if vk, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash); err == nil {
|
||||||
|
ch.current[key] = vk
|
||||||
|
// A check of heads the group no longer holds is stopped: its asks are not put, and it ends superseded.
|
||||||
|
for running, stop := range ch.running[key] {
|
||||||
|
if running != vk {
|
||||||
|
stop(superseded{by: c.HeadsHash})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ch.mu.Unlock()
|
ch.mu.Unlock()
|
||||||
ch.consider(ctx, key, c)
|
ch.consider(ctx, key, c)
|
||||||
}
|
}
|
||||||
@@ -329,12 +350,48 @@ func (ch *Checker) try(ctx context.Context, key string, cs *Case) {
|
|||||||
if cl == nil {
|
if cl == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
run, stop := context.WithCancelCause(ctx)
|
||||||
|
defer stop(nil)
|
||||||
|
ch.mu.Lock()
|
||||||
|
now := ch.current[key]
|
||||||
|
if ch.running[key] == nil {
|
||||||
|
ch.running[key] = map[string]context.CancelCauseFunc{}
|
||||||
|
}
|
||||||
|
ch.running[key][cs.Key] = stop
|
||||||
|
ch.mu.Unlock()
|
||||||
|
defer func() {
|
||||||
|
ch.mu.Lock()
|
||||||
|
delete(ch.running[key], cs.Key)
|
||||||
|
if len(ch.running[key]) == 0 {
|
||||||
|
delete(ch.running, key)
|
||||||
|
}
|
||||||
|
ch.mu.Unlock()
|
||||||
|
}()
|
||||||
|
if now != "" && now != cs.Key {
|
||||||
|
stop(superseded{by: now}) // the group moved on before the check began
|
||||||
|
}
|
||||||
began := ch.now()
|
began := ch.now()
|
||||||
ch.writeOwed(ctx, cs, delivery.Verdict{State: delivery.VerdictRunning}, Outcome{})
|
if run.Err() == nil {
|
||||||
out := ch.judge(ctx, cs)
|
ch.writeOwed(ctx, cs, delivery.Verdict{State: delivery.VerdictRunning}, Outcome{})
|
||||||
|
}
|
||||||
|
out := ch.judge(run, cs)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var sup superseded
|
||||||
|
if errors.As(context.Cause(run), &sup) {
|
||||||
|
// Stopped for newer heads: the verdict ends failed, not running, and no status is put, as the newer check owns the
|
||||||
|
// status of the heads they share.
|
||||||
|
v := delivery.Verdict{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: delivery.VerdictFail,
|
||||||
|
Failed: delivery.FailedFirst{Machine: "checker", Words: sup.Error()}}
|
||||||
|
if data, err := delivery.EncodeVerdict(v); err == nil {
|
||||||
|
if _, err := ch.Verdicts.Update(ctx, cs.Key, data, cl.rev); err != nil {
|
||||||
|
ch.logf("%s: the superseded verdict could not be written: %v", cs.Key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ch.logf("%s: %s", cs.Key, sup.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
v := out.Verdict
|
v := out.Verdict
|
||||||
v.Group, v.Heads, v.Trunk = cs.Group, cs.Heads, cs.Trunk
|
v.Group, v.Heads, v.Trunk = cs.Group, cs.Heads, cs.Trunk
|
||||||
if v.Took.Queue == 0 && !cs.Queued.IsZero() && began.After(cs.Queued) {
|
if v.Took.Queue == 0 && !cs.Queued.IsZero() && began.After(cs.Queued) {
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package check
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// failingResult is a level's test result in which the named tests failed, each in its own package.
|
||||||
|
func failingResult(t *testing.T, tests int, failed ...Failure) json.RawMessage {
|
||||||
|
t.Helper()
|
||||||
|
raw, err := json.Marshal(Tested{Level: delivery.LevelIntegration, Tests: tests, Elapsed: 2, Failed: failed, Passed: len(failed) == 0})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func judgeLevel(t *testing.T, tests ...json.RawMessage) (*delivery.Verdict, *state) {
|
||||||
|
t.Helper()
|
||||||
|
st := &state{j: &Examiner{}, c: &Case{}, levels: map[string]*acc{delivery.LevelUnit: {}, delivery.LevelIntegration: {}}, nodes: map[string]bool{}}
|
||||||
|
v, _ := st.level(t.Context(), &target{pull: Pull{Repository: "novox/x"}}, delivery.LevelAnswer{Level: delivery.LevelIntegration, Tests: tests})
|
||||||
|
return v, st
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue 1921: two failing tests in two packages are two entries of the verdict's Failures and two lines of a try's
|
||||||
|
// failed list, each with its own first line (its cause, issue 1901), whether the build module answered them in one test
|
||||||
|
// result (one go test run over both packages) or in two (the changed packages first, then the rest); every result is
|
||||||
|
// counted, the ones after a failure too; and Failed keeps the first one's words whole.
|
||||||
|
func TestTwoFailingTestsInTwoPackagesAreTwoEntries(t *testing.T) {
|
||||||
|
a := Failure{Package: "x/a", Test: "TestA", Cause: "a_test.go:9: want 1, got 2", Output: " a_test.go:3: logged\n a_test.go:9: want 1, got 2\n--- FAIL: TestA (0.00s)"}
|
||||||
|
b := Failure{Package: "x/b", Test: "TestB", Cause: "b_test.go:4: boom", Output: " b_test.go:4: boom\n--- FAIL: TestB (0.00s)"}
|
||||||
|
for name, tests := range map[string][]json.RawMessage{
|
||||||
|
"one result": {failingResult(t, 10, a, b)},
|
||||||
|
"two results": {failingResult(t, 4, a), failingResult(t, 3), failingResult(t, 3, b)},
|
||||||
|
} {
|
||||||
|
v, st := judgeLevel(t, tests...)
|
||||||
|
if v == nil {
|
||||||
|
t.Fatalf("%s: the level passed", name)
|
||||||
|
}
|
||||||
|
want := []string{"integration: x/a.TestA: a_test.go:9: want 1, got 2", "integration: x/b.TestB: b_test.go:4: boom"}
|
||||||
|
if got := failedLines(*v); strings.Join(got, "\n") != strings.Join(want, "\n") {
|
||||||
|
t.Errorf("%s: failed lines\n%s\nwant\n%s", name, strings.Join(got, "\n"), strings.Join(want, "\n"))
|
||||||
|
}
|
||||||
|
if len(v.Failures) != 2 || v.Failed.Test != "integration: x/a.TestA" || !strings.Contains(v.Failed.Words, "a_test.go:3: logged") {
|
||||||
|
t.Errorf("%s: verdict %+v", name, v)
|
||||||
|
}
|
||||||
|
if run := st.levels[delivery.LevelIntegration].run; run != 10 {
|
||||||
|
t.Errorf("%s: %d tests counted, want all 10", name, run)
|
||||||
|
}
|
||||||
|
if _, err := delivery.EncodeVerdict(delivery.Verdict{Group: "g", Heads: "0123456789ab", State: v.State, Failed: v.Failed, Failures: v.Failures}); err != nil {
|
||||||
|
t.Errorf("%s: the verdict is not written: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue 1921: past delivery.MaxFailures the list names the first fifty and how many more, the status line says how many
|
||||||
|
// failed, and each entry's line is cut to MaxFailureLine.
|
||||||
|
func TestFailuresAreCappedWithHowManyMore(t *testing.T) {
|
||||||
|
var fs []Failure
|
||||||
|
for i := range 60 {
|
||||||
|
fs = append(fs, Failure{Package: fmt.Sprintf("x/p%d", i), Test: "TestT", Cause: strings.Repeat("y", 500)})
|
||||||
|
}
|
||||||
|
v, _ := judgeLevel(t, failingResult(t, 60, fs...))
|
||||||
|
if len(v.Failures) != delivery.MaxFailures+1 || v.Failures[delivery.MaxFailures].Test != "and 10 more" {
|
||||||
|
t.Fatalf("%d entries, last %+v", len(v.Failures), v.Failures[len(v.Failures)-1])
|
||||||
|
}
|
||||||
|
if len(v.Failures[0].Words) > MaxFailureLine {
|
||||||
|
t.Errorf("an entry's line is %d bytes", len(v.Failures[0].Words))
|
||||||
|
}
|
||||||
|
if n := failedCount(*v); n != 60 {
|
||||||
|
t.Errorf("failedCount = %d, want 60", n)
|
||||||
|
}
|
||||||
|
cs := &Case{Pulls: []Pull{{Repository: "novox/x", Head: "0123456789abcdef", Trunk: "fedcba9876543210"}}}
|
||||||
|
if line := Line(*v, cs, cs.Pulls[0], nil, nil, nil); !strings.HasPrefix(line, "failed (60 tests): integration: x/p0.TestT") {
|
||||||
|
t.Errorf("line %q", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A verdict that names no list (a conflict, a refused step, a verdict written before issue 1921) is one failed line, as
|
||||||
|
// before.
|
||||||
|
func TestAVerdictWithoutFailuresIsOneLine(t *testing.T) {
|
||||||
|
v := delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "g14", Words: "files in conflict: go.mod\nmore"}}
|
||||||
|
if got := failedLines(v); len(got) != 1 || got[0] != "g14: files in conflict: go.mod" {
|
||||||
|
t.Fatalf("%q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,18 @@ func (f *fakeSteps) tool(name string) []stepCall {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toolAt is tool's calls of one level: a check's levels run side by side (issue 1921, try speed S10), so only the calls of
|
||||||
|
// one level keep an order.
|
||||||
|
func (f *fakeSteps) toolAt(name, level string) []stepCall {
|
||||||
|
var out []stepCall
|
||||||
|
for _, c := range f.tool(name) {
|
||||||
|
if c.args["level"] == level {
|
||||||
|
out = append(out, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
type entries []delivery.NodeToolchain
|
type entries []delivery.NodeToolchain
|
||||||
|
|
||||||
func (e entries) Entries(language string) []delivery.NodeToolchain {
|
func (e entries) Entries(language string) []delivery.NodeToolchain {
|
||||||
@@ -269,9 +281,9 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
||||||
tests := b.steps.tool("test")
|
tests := b.steps.toolAt("test", "unit")
|
||||||
if len(tests) != 2 || strings.Join(packagesOf(tests[0]), ",") != "fixture/slow" || tests[0].args["level"] != "unit" {
|
if len(tests) != 1 || strings.Join(packagesOf(tests[0]), ",") != "fixture/slow" || len(b.steps.toolAt("test", "integration")) != 1 {
|
||||||
t.Fatalf("test calls = %+v, want the unit level of fixture/slow alone, then the integration level", tests)
|
t.Fatalf("test calls = %+v, want the unit level of fixture/slow alone, and the integration level", b.steps.tool("test"))
|
||||||
}
|
}
|
||||||
if u := out.Verdict.Levels[delivery.LevelUnit]; u.Run != 2 || u.Kept != 40 {
|
if u := out.Verdict.Levels[delivery.LevelUnit]; u.Run != 2 || u.Kept != 40 {
|
||||||
t.Errorf("unit = %+v, want 2 run and 40 kept", u)
|
t.Errorf("unit = %+v, want 2 run and 40 kept", u)
|
||||||
@@ -302,7 +314,7 @@ func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3
|
|||||||
// Asked again with nothing changed: only the failed package runs; the good one is taken.
|
// Asked again with nothing changed: only the failed package runs; the good one is taken.
|
||||||
b.steps.calls = nil
|
b.steps.calls = nil
|
||||||
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
||||||
tests := b.steps.tool("test")
|
tests := b.steps.toolAt("test", "unit")
|
||||||
if len(tests) == 0 || strings.Join(packagesOf(tests[0]), ",") != "fixture/failing/bad" {
|
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)
|
t.Fatalf("the re-ask ran %+v, want only fixture/failing/bad", tests)
|
||||||
}
|
}
|
||||||
@@ -310,7 +322,7 @@ func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3
|
|||||||
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digD}}
|
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digD}}
|
||||||
b.steps.calls = nil
|
b.steps.calls = nil
|
||||||
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
||||||
tests = b.steps.tool("test")
|
tests = b.steps.toolAt("test", "unit")
|
||||||
if got := strings.Join(packagesOf(tests[0]), ","); got != "fixture/failing/bad,fixture/failing/good" && got != "fixture/failing/good,fixture/failing/bad" {
|
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)
|
t.Errorf("after a digest changed the run was %q, want the failed package and the changed one", got)
|
||||||
}
|
}
|
||||||
@@ -341,7 +353,9 @@ func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTheChangesOwnFailingTestRunsFirstAndNothingElseRuns(t *testing.T) { // 1289/AC1, AC2
|
// 1289/AC1, AC2 as issue 1921 changed it: the change's own failing test runs first, the level's other packages still run
|
||||||
|
// after it (so one check names every failure); the later level ran beside it (S10) and is not judged.
|
||||||
|
func TestTheChangesOwnFailingTestRunsFirstThenTheRestAndNoLaterLevel(t *testing.T) {
|
||||||
b := newBench(t)
|
b := newBench(t)
|
||||||
b.fetch.Changed = []string{"modules/m/internal/b/b_test.go"}
|
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}}
|
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
|
||||||
@@ -356,12 +370,12 @@ func TestTheChangesOwnFailingTestRunsFirstAndNothingElseRuns(t *testing.T) { //
|
|||||||
if out.Verdict.State != delivery.VerdictFail || !strings.Contains(out.Verdict.Failed.Test, "TestThreeIsFour") {
|
if out.Verdict.State != delivery.VerdictFail || !strings.Contains(out.Verdict.Failed.Test, "TestThreeIsFour") {
|
||||||
t.Fatalf("verdict = %+v", out.Verdict)
|
t.Fatalf("verdict = %+v", out.Verdict)
|
||||||
}
|
}
|
||||||
tests := b.steps.tool("test")
|
tests := b.steps.toolAt("test", "unit")
|
||||||
if len(tests) != 1 || strings.Join(packagesOf(tests[0]), ",") != "m/internal/b" {
|
if len(tests) != 2 || strings.Join(packagesOf(tests[0]), ",") != "m/internal/b" || strings.Join(packagesOf(tests[1]), ",") != "m/internal/a" {
|
||||||
t.Fatalf("test calls = %+v, want only m/internal/b, once", tests)
|
t.Fatalf("test calls = %+v, want m/internal/b, then m/internal/a, each once", tests)
|
||||||
}
|
}
|
||||||
if n := len(b.steps.tool("digest")); n != 1 {
|
if out.Verdict.Levels[delivery.LevelIntegration].Run != 0 {
|
||||||
t.Errorf("%d digest calls, want the unit level's alone (the check stopped)", n)
|
t.Errorf("the integration level was judged after the unit level failed: %+v", out.Verdict.Levels)
|
||||||
}
|
}
|
||||||
if out.Verdict.Composition == nil || out.Verdict.Composition.State != delivery.CompositionNotRun {
|
if out.Verdict.Composition == nil || out.Verdict.Composition.State != delivery.CompositionNotRun {
|
||||||
t.Errorf("composition = %+v", out.Verdict.Composition)
|
t.Errorf("composition = %+v", out.Verdict.Composition)
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ type target struct {
|
|||||||
runner string
|
runner string
|
||||||
// judged is what the line says of the program that judged a change to the build module; empty for any other change.
|
// judged is what the line says of the program that judged a change to the build module; empty for any other change.
|
||||||
judged string
|
judged string
|
||||||
|
// ask is the check ask put for it, and failed why it could not be asked (issue 1921, try speed S8).
|
||||||
|
ask delivery.StepAsk
|
||||||
|
failed *delivery.Verdict
|
||||||
}
|
}
|
||||||
|
|
||||||
// judgedBy takes what a step under runner workspace answered of the program that ran it (judged_by): the candidate, or the
|
// judgedBy takes what a step under runner workspace answered of the program that ran it (judged_by): the candidate, or the
|
||||||
@@ -307,12 +310,13 @@ func (st *state) levelsOf() []string {
|
|||||||
return []string{delivery.LevelUnit, delivery.LevelIntegration}
|
return []string{delivery.LevelUnit, delivery.LevelIntegration}
|
||||||
}
|
}
|
||||||
|
|
||||||
// judgeAll puts one check ask per pull request and build module the repository needs, the mesh-sdk member first, and
|
// judgeAll puts one check ask per pull request and build module the repository needs, every ask of the group at once so
|
||||||
// judges each answer. A head that does not merge, a pin that is not allowed or a level that fails ends the check at once
|
// the members run side by side on the build nodes (issue 1921, try speed S8), and judges the answers in member order, the
|
||||||
// (ADR 0484 clause 13).
|
// mesh-sdk member first. A head that does not merge, a pin that is not allowed or a level that fails ends the check at once
|
||||||
|
// (ADR 0484 clause 13): the verdict is the first failure in member order, as when the asks were put one after another.
|
||||||
func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
|
func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
|
||||||
// The mesh-sdk member first: whether its merge changes the TypeScript package's paths decides whether the other members'
|
// The mesh-sdk member first: whether its merge changes the TypeScript package's paths decides whether the other members'
|
||||||
// Node fetches take the group's package (ADR 0500 §5).
|
// Node fetches take the group's package (ADR 0500 §5). Those Node asks alone wait for the mesh-sdk member's answer.
|
||||||
pulls := slices.Clone(st.c.Pulls)
|
pulls := slices.Clone(st.c.Pulls)
|
||||||
slices.SortStableFunc(pulls, func(a, b Pull) int {
|
slices.SortStableFunc(pulls, func(a, b Pull) int {
|
||||||
if (a.Repository == SDKRepository) != (b.Repository == SDKRepository) {
|
if (a.Repository == SDKRepository) != (b.Repository == SDKRepository) {
|
||||||
@@ -323,34 +327,32 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
var groupPackage map[string]any
|
sdkGroup := len(st.c.Pulls) > 1 && len(pulls) > 0 && pulls[0].Repository == SDKRepository
|
||||||
for _, p := range pulls {
|
members := make([][]*target, len(pulls))
|
||||||
var merged []Fetched
|
var waiting []*target // Node asks of the other members, put once the mesh-sdk member is judged
|
||||||
|
for i, p := range pulls {
|
||||||
for _, bm := range st.j.Modules.BuildModules(p.Repository) {
|
for _, bm := range st.j.Modules.BuildModules(p.Repository) {
|
||||||
t := &target{pull: p, module: bm}
|
t := st.target(ctx, p, bm)
|
||||||
if tc, none := delivery.AgreedToolchain(st.j.Toolchains.Entries(buildrun.LanguageOf[bm])); none == "" {
|
members[i] = append(members[i], t)
|
||||||
t.toolchain = tc.Version
|
|
||||||
}
|
|
||||||
args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk}
|
|
||||||
if bm == buildrun.NodeBuildModule && groupPackage != nil && p.Repository != SDKRepository {
|
|
||||||
args["package"] = groupPackage
|
|
||||||
}
|
|
||||||
if bm == buildrun.GoBuildModule {
|
|
||||||
beside, err := st.j.besideFor(ctx, st.c, p)
|
|
||||||
if err != nil {
|
|
||||||
return fail(forgeMachine(err, bm), err.Error()+unheardWords(err))
|
|
||||||
}
|
|
||||||
if len(beside) > 0 {
|
|
||||||
args["beside"] = beside
|
|
||||||
t.beside = besideLines(beside)
|
|
||||||
}
|
|
||||||
if p.Repository == catalogueRepository {
|
|
||||||
t.runner = RunnerWorkspace // used by the build module when the merge changes it (ADR 0535 §1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.fetchArgs = args
|
|
||||||
st.targets = append(st.targets, t)
|
st.targets = append(st.targets, t)
|
||||||
if v := st.ask(ctx, t, &merged); v != nil {
|
if t.failed != nil {
|
||||||
|
continue // said when the judging reaches it, so the verdict stays the first failure in member order
|
||||||
|
}
|
||||||
|
if sdkGroup && bm == buildrun.NodeBuildModule && p.Repository != SDKRepository {
|
||||||
|
waiting = append(waiting, t)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.failed = st.put(ctx, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var groupPackage map[string]any
|
||||||
|
for i, p := range pulls {
|
||||||
|
var merged []Fetched
|
||||||
|
for _, t := range members[i] {
|
||||||
|
if t.failed != nil {
|
||||||
|
return t.failed
|
||||||
|
}
|
||||||
|
if v := st.judgeAsk(ctx, t, &merged); v != nil {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,14 +362,51 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
|
|||||||
if p.Repository == SDKRepository && len(st.c.Pulls) > 1 && st.j.changesPackage(p, merged) {
|
if p.Repository == SDKRepository && len(st.c.Pulls) > 1 && st.j.changesPackage(p, merged) {
|
||||||
groupPackage = map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number)}
|
groupPackage = map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number)}
|
||||||
}
|
}
|
||||||
|
if i == 0 && sdkGroup {
|
||||||
|
for _, t := range waiting {
|
||||||
|
if groupPackage != nil {
|
||||||
|
t.fetchArgs["package"] = groupPackage
|
||||||
|
}
|
||||||
|
t.failed = st.put(ctx, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ask puts the target's check ask, waits for the step of the node that pulled it and judges its answer: the fetch, the
|
// target is the check of one pull request by one build module: its fetch's arguments, its toolchain, the siblings its
|
||||||
// pins of its go.mod files, then each level's digest, kept verdicts and tests.
|
// tests read and the runner it is asked with; failed says why it cannot be asked.
|
||||||
func (st *state) ask(ctx context.Context, t *target, merged *[]Fetched) *delivery.Verdict {
|
func (st *state) target(ctx context.Context, p Pull, bm string) *target {
|
||||||
|
t := &target{pull: p, module: bm}
|
||||||
|
if tc, none := delivery.AgreedToolchain(st.j.Toolchains.Entries(buildrun.LanguageOf[bm])); none == "" {
|
||||||
|
t.toolchain = tc.Version
|
||||||
|
}
|
||||||
|
args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk}
|
||||||
|
if bm == buildrun.GoBuildModule {
|
||||||
|
beside, err := st.j.besideFor(ctx, st.c, p)
|
||||||
|
if err != nil {
|
||||||
|
t.failed = fail(forgeMachine(err, bm), err.Error()+unheardWords(err))
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
if len(beside) > 0 {
|
||||||
|
args["beside"] = beside
|
||||||
|
t.beside = besideLines(beside)
|
||||||
|
}
|
||||||
|
if p.Repository == catalogueRepository {
|
||||||
|
t.runner = RunnerWorkspace // used by the build module when the merge changes it (ADR 0535 §1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.fetchArgs = args
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// put puts the target's check ask on its build seat's stream; a check that was stopped (its group holds other heads now,
|
||||||
|
// issue 1791) puts none.
|
||||||
|
func (st *state) put(ctx context.Context, t *target) *delivery.Verdict {
|
||||||
p := t.pull
|
p := t.pull
|
||||||
|
if err := context.Cause(ctx); err != nil {
|
||||||
|
return fail(t.module, fmt.Sprintf("the check of %s#%d was not asked: %v", p.Repository, p.Number, err))
|
||||||
|
}
|
||||||
job := &delivery.CheckJob{Fetch: t.fetchArgs, Levels: st.levelsOf(), Kept: true}
|
job := &delivery.CheckJob{Fetch: t.fetchArgs, Levels: st.levelsOf(), Kept: true}
|
||||||
if t.runner != "" {
|
if t.runner != "" {
|
||||||
job.Runner, job.RunnerWhen = t.runner, buildGoDir
|
job.Runner, job.RunnerWhen = t.runner, buildGoDir
|
||||||
@@ -379,13 +418,20 @@ func (st *state) ask(ctx context.Context, t *target, merged *[]Fetched) *deliver
|
|||||||
if st.c.Kind == KindTrunk {
|
if st.c.Kind == KindTrunk {
|
||||||
kind = delivery.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4)
|
kind = delivery.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4)
|
||||||
}
|
}
|
||||||
a := delivery.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: delivery.CheckerModule,
|
t.ask = delivery.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: delivery.CheckerModule,
|
||||||
At: st.j.now().UTC(), Check: job}
|
At: st.j.now().UTC(), Check: job}
|
||||||
st.c.Progress.Set(fmt.Sprintf("the %s ask %s of %s#%d", kind, a.ID, p.Repository, p.Number), "")
|
st.c.Progress.Set(fmt.Sprintf("the %s ask %s of %s#%d", kind, t.ask.ID, p.Repository, p.Number), "")
|
||||||
if err := st.j.Asks.Put(ctx, a); err != nil {
|
if err := st.j.Asks.Put(ctx, t.ask); err != nil {
|
||||||
return fail(t.module, fmt.Sprintf("the check of %s#%d could not be asked: %v", p.Repository, p.Number, err))
|
return fail(t.module, fmt.Sprintf("the check of %s#%d could not be asked: %v", p.Repository, p.Number, err))
|
||||||
}
|
}
|
||||||
step, err := st.j.Asks.Wait(ctx, a)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// judgeAsk waits for the step of the node that pulled the target's ask and judges its answer: the fetch, the pins of its
|
||||||
|
// go.mod files, then each level's digest, kept verdicts and tests.
|
||||||
|
func (st *state) judgeAsk(ctx context.Context, t *target, merged *[]Fetched) *delivery.Verdict {
|
||||||
|
p := t.pull
|
||||||
|
step, err := st.j.Asks.Wait(ctx, t.ask)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return fail(t.module, ctx.Err().Error())
|
return fail(t.module, ctx.Err().Error())
|
||||||
@@ -427,7 +473,7 @@ func (st *state) ask(ctx context.Context, t *target, merged *[]Fetched) *deliver
|
|||||||
ran := 0.0
|
ran := 0.0
|
||||||
for _, la := range answer.Levels {
|
for _, la := range answer.Levels {
|
||||||
v, elapsed := st.level(ctx, t, la)
|
v, elapsed := st.level(ctx, t, la)
|
||||||
ran += elapsed
|
ran = max(ran, elapsed) // the levels run side by side (issue 1921, try speed S10): the longest is what they took
|
||||||
if v != nil {
|
if v != nil {
|
||||||
st.clone += max(0, took-time.Duration(ran*float64(time.Second)))
|
st.clone += max(0, took-time.Duration(ran*float64(time.Second)))
|
||||||
return v
|
return v
|
||||||
@@ -438,8 +484,9 @@ func (st *state) ask(ctx context.Context, t *target, merged *[]Fetched) *deliver
|
|||||||
}
|
}
|
||||||
|
|
||||||
// level judges one level of a check ask's answer as the tools' answers were judged: the digest keys the kept verdicts
|
// level judges one level of a check ask's answer as the tools' answers were judged: the digest keys the kept verdicts
|
||||||
// of the packages that passed, the kept ones taken are counted, and the first test result that did not pass is the
|
// of the packages that passed, the kept ones taken are counted, and every test result is read and counted, also after
|
||||||
// verdict. It answers the level's running time (its tests' and setup's).
|
// one that did not pass, so the verdict names every failed test of the level (issue 1921). It answers the level's
|
||||||
|
// running time (its tests' and setup's).
|
||||||
func (st *state) level(ctx context.Context, t *target, la delivery.LevelAnswer) (*delivery.Verdict, float64) {
|
func (st *state) level(ctx context.Context, t *target, la delivery.LevelAnswer) (*delivery.Verdict, float64) {
|
||||||
level := la.Level
|
level := la.Level
|
||||||
a := st.levels[level]
|
a := st.levels[level]
|
||||||
@@ -461,6 +508,7 @@ func (st *state) level(ctx context.Context, t *target, la delivery.LevelAnswer)
|
|||||||
a.kept += k.Tests
|
a.kept += k.Tests
|
||||||
}
|
}
|
||||||
elapsed := 0.0
|
elapsed := 0.0
|
||||||
|
var failedResults []Tested
|
||||||
for _, raw := range la.Tests {
|
for _, raw := range la.Tests {
|
||||||
var res Tested
|
var res Tested
|
||||||
if err := json.Unmarshal(raw, &res); err != nil {
|
if err := json.Unmarshal(raw, &res); err != nil {
|
||||||
@@ -489,9 +537,12 @@ func (st *state) level(ctx context.Context, t *target, la delivery.LevelAnswer)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !res.Passed {
|
if !res.Passed {
|
||||||
return levelFailure(t, level, res), elapsed
|
failedResults = append(failedResults, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(failedResults) > 0 {
|
||||||
|
return levelFailure(t, level, failedResults...), elapsed
|
||||||
|
}
|
||||||
return nil, elapsed
|
return nil, elapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,23 +609,64 @@ func firstLine(s string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// failure turns a level that did not pass into a verdict naming what failed first.
|
// MaxFailureLine is the most bytes of a failure's line in a verdict's Failures: the first line of its words, which is
|
||||||
func levelFailure(t *target, level string, res Tested) *delivery.Verdict {
|
// its cause where the build module told one apart (issue 1901). Failed keeps the first failure's words whole.
|
||||||
|
const MaxFailureLine = 300
|
||||||
|
|
||||||
|
// levelFailure turns a level that did not pass into a verdict: Failed names what failed first with its words whole,
|
||||||
|
// and Failures every failed test of the level's results with its first line, at most delivery.MaxFailures and an entry
|
||||||
|
// saying how many more (issue 1921).
|
||||||
|
func levelFailure(t *target, level string, results ...Tested) *delivery.Verdict {
|
||||||
v := delivery.Verdict{State: delivery.VerdictFail}
|
v := delivery.Verdict{State: delivery.VerdictFail}
|
||||||
switch {
|
var all []delivery.FailedFirst
|
||||||
case len(res.Failed) > 0:
|
for _, res := range results {
|
||||||
f := res.Failed[0]
|
if len(res.Failed) == 0 {
|
||||||
name := failedName(f)
|
why := res.Why
|
||||||
if t.module == buildrun.NodeBuildModule && f.Package == "" {
|
if why == "" {
|
||||||
name = res.Module
|
why = "the " + level + " level did not pass"
|
||||||
|
}
|
||||||
|
all = append(all, delivery.FailedFirst{Test: level + ": " + t.pull.Repository, Words: why})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
v.Failed = delivery.FailedFirst{Test: level + ": " + name, Words: failureWords(f)}
|
for _, f := range res.Failed {
|
||||||
default:
|
name := failedName(f)
|
||||||
why := res.Why
|
if t.module == buildrun.NodeBuildModule && f.Package == "" {
|
||||||
if why == "" {
|
name = res.Module
|
||||||
why = "the " + level + " level did not pass"
|
}
|
||||||
|
step := level
|
||||||
|
if f.Step != "" {
|
||||||
|
step = f.Step // the lint step's finding, not the level's (ADR 0542 §2)
|
||||||
|
}
|
||||||
|
all = append(all, delivery.FailedFirst{Test: step + ": " + name, Words: failureWords(f)})
|
||||||
}
|
}
|
||||||
v.Failed = delivery.FailedFirst{Test: level + ": " + t.pull.Repository, Words: why}
|
|
||||||
}
|
}
|
||||||
|
if len(all) == 0 { // no result: a level said failed with nothing to name
|
||||||
|
all = append(all, delivery.FailedFirst{Test: level + ": " + t.pull.Repository, Words: "the " + level + " level did not pass"})
|
||||||
|
}
|
||||||
|
v.Failed = all[0]
|
||||||
|
v.Failures = failureLines(all)
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// failureLines is every failure with the first line of its words, cut to MaxFailureLine, at most delivery.MaxFailures
|
||||||
|
// and then one entry saying how many more failed.
|
||||||
|
func failureLines(all []delivery.FailedFirst) []delivery.FailedFirst {
|
||||||
|
n := min(len(all), delivery.MaxFailures)
|
||||||
|
out := make([]delivery.FailedFirst, 0, n+1)
|
||||||
|
for _, f := range all[:n] {
|
||||||
|
out = append(out, delivery.FailedFirst{Test: f.Test, Machine: f.Machine, Words: clipLine(firstLine(f.Words), MaxFailureLine)})
|
||||||
|
}
|
||||||
|
if more := len(all) - n; more > 0 {
|
||||||
|
out = append(out, delivery.FailedFirst{Test: fmt.Sprintf("and %d more", more)})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// clipLine cuts a line to at most n bytes on a whole character, marking the cut.
|
||||||
|
func clipLine(s string, n int) string {
|
||||||
|
if len(s) <= n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s = strings.ToValidUTF8(s[:n-3], "")
|
||||||
|
return s + "..."
|
||||||
|
}
|
||||||
|
|||||||
@@ -177,8 +177,8 @@ func TestACandidateThatDoesNotCompileFailsTheCheckWithTheCompilersWords(t *testi
|
|||||||
if out.Verdict.State != delivery.VerdictFail || !strings.Contains(out.Verdict.Failed.Words, "undefined: x") {
|
if out.Verdict.State != delivery.VerdictFail || !strings.Contains(out.Verdict.Failed.Words, "undefined: x") {
|
||||||
t.Fatalf("verdict = %+v, want a failure naming the compile error", out.Verdict)
|
t.Fatalf("verdict = %+v, want a failure naming the compile error", out.Verdict)
|
||||||
}
|
}
|
||||||
if n := len(b.steps.tool("test")); n != 1 {
|
if n := len(b.steps.toolAt("test", "unit")); n != 1 {
|
||||||
t.Errorf("%d test steps, want the first one's failure to end the check", n)
|
t.Errorf("%d unit test steps, want the first one's failure to end the level", n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,34 @@ func firstFailed(v delivery.Verdict) string {
|
|||||||
return firstWordsLine(f.Words)
|
return firstWordsLine(f.Words)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// failedLines is everything a verdict names as failed, one line each: every failed test with its first line (issue
|
||||||
|
// 1921), or what failed first when the verdict names no list (a conflict, a refused step, an older verdict).
|
||||||
|
func failedLines(v delivery.Verdict) []string {
|
||||||
|
if len(v.Failures) == 0 {
|
||||||
|
if f := firstFailed(v); f != "" {
|
||||||
|
return []string{f}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lines := make([]string, 0, len(v.Failures))
|
||||||
|
for _, f := range v.Failures {
|
||||||
|
lines = append(lines, firstFailed(delivery.Verdict{Failed: f}))
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// failedCount is how many tests a verdict names as failed, the ones after delivery.MaxFailures counted too.
|
||||||
|
func failedCount(v delivery.Verdict) int {
|
||||||
|
n := len(v.Failures)
|
||||||
|
if n > delivery.MaxFailures {
|
||||||
|
var more int
|
||||||
|
if _, err := fmt.Sscanf(v.Failures[n-1].Test, "and %d more", &more); err == nil {
|
||||||
|
return delivery.MaxFailures + more
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
// firstWordsLine is the first line of a failure's words: the words are several lines (issue 1901) and the status line one.
|
// firstWordsLine is the first line of a failure's words: the words are several lines (issue 1901) and the status line one.
|
||||||
func firstWordsLine(w string) string { return firstLine(w) }
|
func firstWordsLine(w string) string { return firstLine(w) }
|
||||||
|
|
||||||
@@ -155,6 +183,9 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s
|
|||||||
pc.head = "pass " + commits
|
pc.head = "pass " + commits
|
||||||
default:
|
default:
|
||||||
pc.head = "failed: " + firstFailed(v)
|
pc.head = "failed: " + firstFailed(v)
|
||||||
|
if n := failedCount(v); n > 1 {
|
||||||
|
pc.head = fmt.Sprintf("failed (%d tests): %s", n, firstFailed(v))
|
||||||
|
}
|
||||||
pc.commits = commits
|
pc.commits = commits
|
||||||
}
|
}
|
||||||
var counts []string
|
var counts []string
|
||||||
|
|||||||
@@ -118,8 +118,10 @@ func (st *state) checkPinsOf(ctx context.Context, t *target) verdictPtr {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sameBranchPin names the branch of the tried repository that points at the tried head and that mesh-sdk also has, at the
|
// sameBranchPin names the mesh-sdk branch whose head the pin is, for a try: the branch of the tried repository that points
|
||||||
// commit the pin names; empty when there is none.
|
// at the tried head and that mesh-sdk also has, at the commit the pin names, first; else any branch of mesh-sdk but its
|
||||||
|
// trunk whose head the pin is, for a try asked by a bare commit that no branch of the tried repository points at (issue
|
||||||
|
// 1624; try speed S12). Empty when there is none.
|
||||||
func (st *state) sameBranchPin(ctx context.Context, t *target, pin string) (string, error) {
|
func (st *state) sameBranchPin(ctx context.Context, t *target, pin string) (string, error) {
|
||||||
mine, err := st.j.Forge.BranchHeads(ctx, t.pull.Repository)
|
mine, err := st.j.Forge.BranchHeads(ctx, t.pull.Repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -131,9 +133,6 @@ func (st *state) sameBranchPin(ctx context.Context, t *target, pin string) (stri
|
|||||||
same = append(same, name)
|
same = append(same, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(same) == 0 {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
sdk, err := st.j.Forge.BranchHeads(ctx, SDKRepository)
|
sdk, err := st.j.Forge.BranchHeads(ctx, SDKRepository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -144,6 +143,16 @@ func (st *state) sameBranchPin(ctx context.Context, t *target, pin string) (stri
|
|||||||
return name, nil
|
return name, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var any []string
|
||||||
|
for name, h := range sdk {
|
||||||
|
if name != "main" && strings.HasPrefix(h, pin) {
|
||||||
|
any = append(any, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(any)
|
||||||
|
if len(any) > 0 {
|
||||||
|
return any[0], nil
|
||||||
|
}
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -408,3 +408,22 @@ func TestAMergeCheckWithTheSameBranchPinIsStillRefusedWithoutAGroupPullRequest(t
|
|||||||
t.Fatalf("verdict = %+v", v)
|
t.Fatalf("verdict = %+v", v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue 1624 (met again on 2026-10-25; try speed S12): a try asked by a bare commit, which no branch of the tried
|
||||||
|
// repository points at, still takes a pin that is the head of a mesh-sdk branch, naming that branch; a pin that is the head
|
||||||
|
// of no mesh-sdk branch is still refused.
|
||||||
|
func TestATryByCommitTakesAPinThatIsTheHeadOfAnSDKBranch(t *testing.T) {
|
||||||
|
b, c := sameBranchBench(t, head2[:12])
|
||||||
|
b.ex.Forge.(*fakeForge).branches[repoA] = map[string]string{"main": trunkA, "feat/r6": trunkA2} // the branch moved on
|
||||||
|
if v := b.judge(t, c).Verdict; v.State != delivery.VerdictPass {
|
||||||
|
t.Fatalf("verdict = %+v", v)
|
||||||
|
}
|
||||||
|
if n := c.Notes(); len(n) != 1 || n[0] != "pin accepted: head of mesh-sdk feat/r6" {
|
||||||
|
t.Errorf("notes = %q", n)
|
||||||
|
}
|
||||||
|
b, c = sameBranchBench(t, "9aaa5c80ac5c")
|
||||||
|
b.ex.Forge.(*fakeForge).branches[repoA] = map[string]string{"main": trunkA}
|
||||||
|
if v := b.judge(t, c).Verdict; v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "neither on mesh-sdk's trunk") {
|
||||||
|
t.Errorf("a pin no mesh-sdk branch holds: verdict = %+v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ type Failure struct {
|
|||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
// Cause is the first line of what the test reported as its error, when the build module told it apart (issue 1901).
|
// Cause is the first line of what the test reported as its error, when the build module told it apart (issue 1901).
|
||||||
Cause string `json:"cause,omitempty"`
|
Cause string `json:"cause,omitempty"`
|
||||||
|
// Step is "lint" for a finding of the lint step (gofmt, go vet), which runs beside the unit level and is reported as
|
||||||
|
// its own failure, not the level's (ADR 0542 §2).
|
||||||
|
Step string `json:"step,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTime is one test and the seconds it took.
|
// TestTime is one test and the seconds it took.
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package check
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
const repoC = "novox/mesh-controller"
|
||||||
|
|
||||||
|
func statusOf(t *testing.T, r *rig, repo string, n int, head string) string {
|
||||||
|
t.Helper()
|
||||||
|
key, _ := delivery.CheckerOwedKey(repo, n, head)
|
||||||
|
raw, _, found, _ := r.owed.Get(context.Background(), key)
|
||||||
|
if !found {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
o, err := delivery.DecodeCheckerOwed(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return o.State
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1791 note 9 (issue 1921, try speed S9): two of a group's three members merge. The candidate keeps them, marked merged,
|
||||||
|
// with the same heads; their trunks moved by their own merges. No new check is asked, and the member left keeps its
|
||||||
|
// passed status, so the merge of the rest is not refused.
|
||||||
|
func TestTwoMembersMergingAskNoNewCheckAndTheThirdKeepsItsPass(t *testing.T) {
|
||||||
|
ctx, j := context.Background(), passJudge()
|
||||||
|
r := newRig(t, j)
|
||||||
|
r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS, repoC: trunkA})
|
||||||
|
a, s, c := pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2), pullOf(repoC, 3, head1)
|
||||||
|
r.ch.Candidate(ctx, "feat%2Fx", candidateOf(t, "feat/x", a, s, c))
|
||||||
|
r.ch.Wait()
|
||||||
|
if j.count() != 1 || statusOf(t, r, repoC, 3, head1) != "success" {
|
||||||
|
t.Fatalf("judged %d times, the third's status %q", j.count(), statusOf(t, r, repoC, 3, head1))
|
||||||
|
}
|
||||||
|
a.Merged, s.Merged = true, true
|
||||||
|
r.ch.Candidate(ctx, "feat%2Fx", candidateOf(t, "feat/x", a, s, c))
|
||||||
|
r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS2, repoC: trunkA})
|
||||||
|
r.ch.TrunkMoved(ctx)
|
||||||
|
r.ch.Wait()
|
||||||
|
if j.count() != 1 {
|
||||||
|
t.Errorf("a new check was asked after two members merged: judged %d times", j.count())
|
||||||
|
}
|
||||||
|
if got := statusOf(t, r, repoC, 3, head1); got != "success" {
|
||||||
|
t.Errorf("the third member's status is %q, want success", got)
|
||||||
|
}
|
||||||
|
// The trunk of the member left moves: that is a re-judge as before.
|
||||||
|
r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS2, repoC: trunkA2})
|
||||||
|
r.ch.TrunkMoved(ctx)
|
||||||
|
r.ch.Wait()
|
||||||
|
if j.count() != 2 {
|
||||||
|
t.Errorf("the member left's trunk moved and it was judged %d times, want again", j.count())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1791 note 8 (issue 1921, try speed S9): a group opened member by member writes a candidate for each partial set. When
|
||||||
|
// the group's candidate moves to new heads, the check of the older heads is stopped and ends failed, superseded, instead
|
||||||
|
// of running on or staying running; it puts no status, as the newer check owns the heads they share.
|
||||||
|
func TestACheckWhoseHeadsTheGroupNoLongerHoldsIsStoppedAndEndsSuperseded(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
started := make(chan struct{})
|
||||||
|
j := &judgeFunc{f: func(ctx context.Context, c *Case, call int) (Outcome, error) {
|
||||||
|
if len(c.Pulls) == 1 {
|
||||||
|
close(started)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return Outcome{Verdict: failure("checker", ctx.Err().Error())}, nil
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
return Outcome{Verdict: passing()}, nil // never stopped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Outcome{Verdict: passing()}, nil
|
||||||
|
}}
|
||||||
|
r := newRig(t, j)
|
||||||
|
partial := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
||||||
|
whole := candidateOf(t, "feat/x", pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))
|
||||||
|
r.ch.Candidate(ctx, "feat%2Fx", partial)
|
||||||
|
<-started
|
||||||
|
r.ch.Candidate(ctx, "feat%2Fx", whole)
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { r.ch.Wait(); close(done) }()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("the check of the older heads was not stopped")
|
||||||
|
}
|
||||||
|
old := r.verdict(t, keyOf(t, "feat/x", partial))
|
||||||
|
if old.State != delivery.VerdictFail || !strings.Contains(old.Failed.Words, "superseded") {
|
||||||
|
t.Errorf("the older heads' verdict = %+v, want failed, superseded", old)
|
||||||
|
}
|
||||||
|
if v := r.verdict(t, keyOf(t, "feat/x", whole)); v.State != delivery.VerdictPass {
|
||||||
|
t.Errorf("the newer heads' verdict = %+v", v)
|
||||||
|
}
|
||||||
|
if got := statusOf(t, r, repoA, 7, head1); got != "success" {
|
||||||
|
t.Errorf("the shared head's status is %q, want the newer check's success", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,9 @@ type Try struct {
|
|||||||
// Said is the try's report in words: what failed first, the tests run, kept and skipped per level, the time of each part.
|
// Said is the try's report in words: what failed first, the tests run, kept and skipped per level, the time of each part.
|
||||||
Said string `json:"said,omitempty"`
|
Said string `json:"said,omitempty"`
|
||||||
// Took is the time from its start to its end, as Go writes a duration.
|
// Took is the time from its start to its end, as Go writes a duration.
|
||||||
Took string `json:"took,omitempty"`
|
Took string `json:"took,omitempty"`
|
||||||
|
// Failed is every test that failed, each with its first line, at most delivery.MaxFailures and then how many more
|
||||||
|
// (issue 1921); or what else failed first (a conflict, a refused step).
|
||||||
Failed []string `json:"failed,omitempty"`
|
Failed []string `json:"failed,omitempty"`
|
||||||
Levels map[string]delivery.LevelResult `json:"levels,omitempty"`
|
Levels map[string]delivery.LevelResult `json:"levels,omitempty"`
|
||||||
Machines []string `json:"machines,omitempty"`
|
Machines []string `json:"machines,omitempty"`
|
||||||
@@ -339,9 +341,7 @@ func (tr *Tryer) run(ctx context.Context, key string) {
|
|||||||
t.State = TryPass
|
t.State = TryPass
|
||||||
default:
|
default:
|
||||||
t.State = TryFail
|
t.State = TryFail
|
||||||
if f := firstFailed(v); f != "" {
|
t.Failed = failedLines(v)
|
||||||
t.Failed = []string{f}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if t.Tests != "" {
|
if t.Tests != "" {
|
||||||
t.Said += " | only the tests " + t.Tests + " (Go); nothing kept"
|
t.Said += " | only the tests " + t.Tests + " (Go); nothing kept"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-indexer
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95
|
||||||
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,11 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468 h1:UcMee+IV2ii5E4dRZdQUaSaeh9IedTFYumuQbBiB0k0=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468 h1:UcMee+IV2ii5E4dRZdQUaSaeh9IedTFYumuQbBiB0k0=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024231716-571ac521f053 h1:8Kbp3aJG3jGndDOfmGdMSDgJVEcc1hyu921hVnnlnlQ=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024231716-571ac521f053/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232250-9b408db24a58 h1:4Damtdqlf6u6nJhvMA99peDy1jmjixtcwFXJ4bWEkLk=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232250-9b408db24a58/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95 h1:I8zbgGzRjmgYydPTWo8hmaZW/P/g+qGDUfu3E32/pkU=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95/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=
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func (ix *Indexer) logf(format string, args ...any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply takes one message of the bridge's stream FORGE into the group of its branch. A pull message adds or moves the
|
// Apply takes one message of the bridge's stream FORGE into the group of its branch. A pull message adds or moves the
|
||||||
// pull request, a closed one or a merged message takes it out; a group left with none is deleted. A change is written
|
// pull request, a closed one takes it out, a merged one marks it merged; a group left with none open is deleted. A change is written
|
||||||
// by compare-and-set on the revision read, and read again on a conflict. A lone pull request is a group of one. A
|
// by compare-and-set on the revision read, and read again on a conflict. A lone pull request is a group of one. A
|
||||||
// merged message also writes the fingerprint of each module of its repository (modules.go).
|
// merged message also writes the fingerprint of each module of its repository (modules.go).
|
||||||
func (ix *Indexer) Apply(ctx context.Context, f delivery.Forge) error {
|
func (ix *Indexer) Apply(ctx context.Context, f delivery.Forge) error {
|
||||||
@@ -154,12 +154,13 @@ func (ix *Indexer) applyOpen(ctx context.Context, f delivery.Forge) error {
|
|||||||
return nil, false, fmt.Errorf("candidates.%s holds what is not a candidate: %w", key, err)
|
return nil, false, fmt.Errorf("candidates.%s holds what is not a candidate: %w", key, err)
|
||||||
}
|
}
|
||||||
kept := slices.DeleteFunc(slices.Clone(c.Pulls), func(p delivery.CandidatePull) bool {
|
kept := slices.DeleteFunc(slices.Clone(c.Pulls), func(p delivery.CandidatePull) bool {
|
||||||
return !named[delivery.OpenPull{Repository: p.Repository, Number: p.Number}]
|
// a member that merged is open no more, and stays while another member is (issue 1791)
|
||||||
|
return !p.Merged && !named[delivery.OpenPull{Repository: p.Repository, Number: p.Number}]
|
||||||
})
|
})
|
||||||
switch {
|
switch {
|
||||||
case len(kept) == len(c.Pulls):
|
case len(kept) == len(c.Pulls):
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
case len(kept) == 0:
|
case !anyOpen(kept):
|
||||||
ix.logf("%s: no pull request of the group is open, so no candidate", key)
|
ix.logf("%s: no pull request of the group is open, so no candidate", key)
|
||||||
return nil, true, nil
|
return nil, true, nil
|
||||||
}
|
}
|
||||||
@@ -232,7 +233,16 @@ func (ix *Indexer) next(cur *delivery.Candidate, f delivery.Forge, digests map[s
|
|||||||
if at < 0 {
|
if at < 0 {
|
||||||
return cur
|
return cur
|
||||||
}
|
}
|
||||||
pulls = slices.Delete(pulls, at, at+1)
|
if f.Kind == delivery.ForgeMerged {
|
||||||
|
// A member that merges stays, marked merged, with the head it merged at: the heads hash the group's Approve
|
||||||
|
// covers holds, so the checker starts no new check for the members still open (issue 1791; issue 1921 S9).
|
||||||
|
pulls[at].Merged = true
|
||||||
|
} else {
|
||||||
|
pulls = slices.Delete(pulls, at, at+1)
|
||||||
|
}
|
||||||
|
if !anyOpen(pulls) {
|
||||||
|
return nil // every member merged or closed: the group is done
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
entry := delivery.CandidatePull{Head: delivery.Head{Repository: f.Repository, Number: f.Number, Head: f.Head}, Modules: []string{}}
|
entry := delivery.CandidatePull{Head: delivery.Head{Repository: f.Repository, Number: f.Number, Head: f.Head}, Modules: []string{}}
|
||||||
switch {
|
switch {
|
||||||
@@ -276,6 +286,11 @@ func (ix *Indexer) next(cur *delivery.Candidate, f delivery.Forge, digests map[s
|
|||||||
return &delivery.Candidate{V: delivery.Version, Group: f.Branch, Base: base, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}
|
return &delivery.Candidate{V: delivery.Version, Group: f.Branch, Base: base, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// anyOpen says whether a group's pull requests hold one that has not merged.
|
||||||
|
func anyOpen(pulls []delivery.CandidatePull) bool {
|
||||||
|
return slices.ContainsFunc(pulls, func(p delivery.CandidatePull) bool { return !p.Merged })
|
||||||
|
}
|
||||||
|
|
||||||
// NoneNoPreview is why a candidate's touched module has no fingerprint of the new form: the merge preview is not
|
// NoneNoPreview is why a candidate's touched module has no fingerprint of the new form: the merge preview is not
|
||||||
// computed in this release (novox/hq ADR 0465 clause 3; the checker comes with issue 1445).
|
// computed in this release (novox/hq ADR 0465 clause 3; the checker comes with issue 1445).
|
||||||
const NoneNoPreview = "no merge preview is computed"
|
const NoneNoPreview = "no merge preview is computed"
|
||||||
|
|||||||
@@ -206,8 +206,8 @@ func TestDirsNotKnownKeepTheModulesOfTheSameHead(t *testing.T) { // 1238/AC4
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClosedAndMergedTakeAPullOutAndTheLastDeletesTheKey(t *testing.T) { // 1238/AC5
|
func TestClosedTakesAPullOutAndTheLastDeletesTheKey(t *testing.T) { // 1238/AC5 (a merge: TestAMergedMemberStays...)
|
||||||
for _, how := range []string{"closed", "merged"} {
|
for _, how := range []string{"closed"} {
|
||||||
ix, st, _ := newIndexer(t)
|
ix, st, _ := newIndexer(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_ = ix.Apply(ctx, pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"))
|
_ = ix.Apply(ctx, pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"))
|
||||||
@@ -387,3 +387,70 @@ func TestAnOpenMessageDropsEveryCandidateItDoesNotName(t *testing.T) { // 1279/A
|
|||||||
t.Errorf("after an empty open message the candidates hold %v", st.vals)
|
t.Errorf("after an empty open message the candidates hold %v", st.vals)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1791 (issue 1921, try speed S9): a member of a group that merges while another is open stays in the candidate, marked
|
||||||
|
// merged, with its head: the heads hash the Approve covers holds, so no new check starts for the members left. An open
|
||||||
|
// message does not drop it, a close of the last open member deletes the group, and the last merge does too.
|
||||||
|
func TestAMergedMemberStaysInItsGroupUntilEveryMemberMerged(t *testing.T) {
|
||||||
|
merged := func(repo string, n int, head string) delivery.Forge {
|
||||||
|
return delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: repo, Branch: "feat/x", Number: n, Head: head, Merge: h3, Root: "r", Trees: map[string]string{"a": "t"}}
|
||||||
|
}
|
||||||
|
ix, st, _ := newIndexer(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, f := range []delivery.Forge{pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"), pull("novox/mesh-catalog", 300, "feat/x", h2, "modules/c"),
|
||||||
|
pull("novox/mesh-sdk", 7, "feat/x", h3)} {
|
||||||
|
if err := ix.Apply(ctx, f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
before, _ := read(t, st, "feat/x")
|
||||||
|
if err := ix.Apply(ctx, merged("novox/mesh-sdk", 7, h3)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ix.Apply(ctx, merged("novox/mesh-delivery", 12, h1)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c, ok := read(t, st, "feat/x")
|
||||||
|
if !ok || len(c.Pulls) != 3 || c.HeadsHash != before.HeadsHash {
|
||||||
|
t.Fatalf("after two merges the candidate = %+v (kept %v), want the three members and the same heads hash", c, ok)
|
||||||
|
}
|
||||||
|
for _, p := range c.Pulls {
|
||||||
|
if want := p.Repository != "novox/mesh-catalog"; p.Merged != want {
|
||||||
|
t.Errorf("%s#%d merged = %v, want %v", p.Repository, p.Number, p.Merged, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The open message names only the member still open: the merged ones stay.
|
||||||
|
if err := ix.Apply(ctx, open(delivery.OpenPull{Repository: "novox/mesh-catalog", Number: 300})); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c, _ := read(t, st, "feat/x"); len(c.Pulls) != 3 || c.HeadsHash != before.HeadsHash {
|
||||||
|
t.Errorf("the open message dropped a merged member: %+v", c)
|
||||||
|
}
|
||||||
|
// The last open member merges: the group is done.
|
||||||
|
if err := ix.Apply(ctx, merged("novox/mesh-catalog", 300, h2)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := read(t, st, "feat/x"); ok {
|
||||||
|
t.Error("every member merged and the group is still a candidate")
|
||||||
|
}
|
||||||
|
// A close of the last open member deletes the group too.
|
||||||
|
ix, st, _ = newIndexer(t)
|
||||||
|
_ = ix.Apply(ctx, pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"))
|
||||||
|
_ = ix.Apply(ctx, pull("novox/mesh-catalog", 300, "feat/x", h2, "modules/c"))
|
||||||
|
_ = ix.Apply(ctx, merged("novox/mesh-delivery", 12, h1))
|
||||||
|
closed := pull("novox/mesh-catalog", 300, "feat/x", h2)
|
||||||
|
closed.Action = delivery.PullClosed
|
||||||
|
if err := ix.Apply(ctx, closed); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := read(t, st, "feat/x"); ok {
|
||||||
|
t.Error("the last open member closed and the group, holding only a merged one, is still a candidate")
|
||||||
|
}
|
||||||
|
// A lone pull request that merges leaves no group.
|
||||||
|
ix, st, _ = newIndexer(t)
|
||||||
|
_ = ix.Apply(ctx, pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"))
|
||||||
|
_ = ix.Apply(ctx, merged("novox/mesh-delivery", 12, h1))
|
||||||
|
if _, ok := read(t, st, "feat/x"); ok {
|
||||||
|
t.Error("a lone merged pull request is still a candidate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user