diff --git a/modules/mesh-delivery-checker/internal/check/adr548_test.go b/modules/mesh-delivery-checker/internal/check/adr548_test.go index dc42f96..b4db1a8 100644 --- a/modules/mesh-delivery-checker/internal/check/adr548_test.go +++ b/modules/mesh-delivery-checker/internal/check/adr548_test.go @@ -43,6 +43,14 @@ func waitFor(t testing.TB, what string, ok func() bool) { } } +// pullWithFP is a pull request touching the module m at the fingerprint `src2:` and 64 times the hex digit d. +func pullWithFP(repo string, number int, head string, d string) delivery.CandidatePull { + p := pullOf(repo, number, head) + p.Modules = []string{"m"} + p.Fps = map[string]delivery.CandidateModule{"m": {FP2: "src2:" + strings.Repeat(d, 64)}} + return p +} + func TestOneCheckPerDelivery(t *testing.T) { ctx := context.Background() for name, run := range map[string]func(t *testing.T){ @@ -145,6 +153,30 @@ func TestOneCheckPerDelivery(t *testing.T) { t.Error("the verdict key was made again: a carry rewrites it") } }, + // §5: the composition is kept when no fingerprint of a module it composed changed, and runs again otherwise. + "a carry keeps a passed composition while the fingerprints stay the same": func(t *testing.T) { + var kept []bool + j := &judgeFunc{f: func(_ context.Context, c *Case, _ int) (Outcome, error) { + kept = append(kept, c.Composition != nil) + v := passing() + v.Composition = &delivery.Composition{State: delivery.CompositionPass, SDK: "v0.1.14-x", FPSet: c.FPSet} + return Outcome{Verdict: v}, nil + }} + r := newRig(t, j) + cand := candidateOf(t, "feat/x", pullWithFP(repoA, 7, head1, "a")) + r.ch.Candidate(ctx, "feat%2Fx", cand) + r.ch.Wait() + r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) + r.ch.TrunkMoved(ctx) // the indexer's fingerprints of the candidate stay the same + r.ch.Wait() + r.ch.Candidate(ctx, "feat%2Fx", candidateOf(t, "feat/x", pullWithFP(repoA, 7, head1, "b"))) // a fingerprint changed with the move + r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS}) + r.ch.TrunkMoved(ctx) + r.ch.Wait() + if len(kept) != 3 || kept[0] || !kept[1] || kept[2] { + t.Errorf("the composition was kept in the checks %v; want composed, kept on the carry, composed again once a fingerprint changed", kept) + } + }, // §5: a carry that fails names the move it failed on. "a carry that fails names the move": func(t *testing.T) { j := &judgeFunc{f: func(_ context.Context, c *Case, call int) (Outcome, error) { diff --git a/modules/mesh-delivery-checker/internal/check/awaiting.go b/modules/mesh-delivery-checker/internal/check/awaiting.go index 1fbae2c..74abfda 100644 --- a/modules/mesh-delivery-checker/internal/check/awaiting.go +++ b/modules/mesh-delivery-checker/internal/check/awaiting.go @@ -127,6 +127,9 @@ func (ch *Checker) Composed(ctx context.Context, comp delivery.CompositionVerdic return } withComposition(&v, comp) + if v.Composition != nil && v.Composition.State != delivery.CompositionNotRun { + v.Composition.FPSet = cs.FPSet + } data, err := delivery.EncodeVerdict(v) if err != nil { ch.logf("%s: the verdict with its composition cannot be written: %v", cs.Key, err) diff --git a/modules/mesh-delivery-checker/internal/check/case.go b/modules/mesh-delivery-checker/internal/check/case.go index 8f5fe53..1d4ec4d 100644 --- a/modules/mesh-delivery-checker/internal/check/case.go +++ b/modules/mesh-delivery-checker/internal/check/case.go @@ -52,6 +52,11 @@ type Case struct { // CarriedFrom are, for the carry of a verdict that passed, the trunk and sibling commits it was judged on (ADR 0548 §5); // nil for a whole check. CarriedFrom map[string]string + // FPSet is the candidate's fingerprint set (Candidate.FPSet): a composition names the set it judged. + FPSet string + // Composition is, for a carry, the composition that passed on the same fingerprint set: it is kept and not asked again, + // since no fingerprint of a module it composed changed (ADR 0548 §5). Nil composes. + Composition *delivery.Composition } // Progress is the step a check is at, written by the check and read when its bound passes. @@ -83,7 +88,7 @@ func CaseOf(c delivery.Candidate, trunk map[string]string) *Case { if err != nil { return nil } - 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{}, FPSet: c.FPSet} for _, p := range c.Pulls { switch head, known := trunk[p.Repository]; { case !known || head == "": diff --git a/modules/mesh-delivery-checker/internal/check/checker.go b/modules/mesh-delivery-checker/internal/check/checker.go index 02b5115..af8cd96 100644 --- a/modules/mesh-delivery-checker/internal/check/checker.go +++ b/modules/mesh-delivery-checker/internal/check/checker.go @@ -232,7 +232,7 @@ const ( // commits are named as the carry's CarriedFrom. A verdict that stands is ranked with the carries: its claim finds nothing // to do. func (ch *Checker) prepare(ctx context.Context, cs *Case) { - cs.Kind, cs.CarriedFrom = KindNew, nil + cs.Kind, cs.CarriedFrom, cs.Composition = KindNew, nil, nil value, _, found, err := ch.Verdicts.Get(ctx, cs.Key) if err != nil || !found { return @@ -248,6 +248,10 @@ func (ch *Checker) prepare(ctx context.Context, cs *Case) { cs.Kind = KindTrunk if v.State == delivery.VerdictPass && ch.stale(v, cs) { cs.CarriedFrom = judgedOn(v) + if c := v.Composition; c != nil && c.State == delivery.CompositionPass && c.FPSet != "" && c.FPSet == cs.FPSet { + kept := *c + cs.Composition = &kept + } } } diff --git a/modules/mesh-delivery-checker/internal/check/composition_test.go b/modules/mesh-delivery-checker/internal/check/composition_test.go index 19bb6b6..9320456 100644 --- a/modules/mesh-delivery-checker/internal/check/composition_test.go +++ b/modules/mesh-delivery-checker/internal/check/composition_test.go @@ -28,6 +28,7 @@ func TestAPassingCheckTakesTheAssignersComposition(t *testing.T) { f := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "v0.1.14-x", Machines: []string{"g14"}, Took: 40}} b.ex.Compositions = f cs := caseOf(t, pullOf(repoA, 7, head1)) + cs.FPSet = "fps-of-the-candidate" v := b.judge(t, cs).Verdict if v.State != delivery.VerdictPass || v.Composition == nil || v.Composition.State != delivery.CompositionPass || v.Composition.SDK != "v0.1.14-x" || v.Took.Composition != 40 { t.Fatalf("verdict = %+v, composition %+v", v, v.Composition) @@ -38,6 +39,15 @@ func TestAPassingCheckTakesTheAssignersComposition(t *testing.T) { if !compositionMentions(v, "passed") { t.Errorf("the line says %q", compositionWords(v.Composition, v.Took.Composition)) } + if v.Composition.FPSet == "" || v.Composition.FPSet != cs.FPSet { + t.Errorf("the composition names the fingerprint set %q, want the candidate's %q (ADR 0548 §5)", v.Composition.FPSet, cs.FPSet) + } + // ADR 0548 §5: a carry whose fingerprints did not change keeps the composition it passed and asks the assigner nothing. + carry := caseOf(t, pullOf(repoA, 7, head1)) + carry.CarriedFrom, carry.Composition = map[string]string{repoA: trunkA2}, v.Composition + if kept := b.judge(t, carry).Verdict; kept.Composition == nil || kept.Composition.SDK != "v0.1.14-x" || len(f.asked) != 1 { + t.Errorf("the carry's composition is %+v and the assigner was asked %d times; want the kept one and no second ask", kept.Composition, len(f.asked)) + } } func compositionMentions(v delivery.Verdict, word string) bool { diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index 66a7fcc..0687a57 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -230,6 +230,13 @@ func (st *state) finish(ctx context.Context, v delivery.Verdict) Outcome { // fails the check, naming the machine it failed at. A try is not composed (it has no candidate); a composition that // cannot be had leaves it not run, said; with no assigner running it is not judged, said at once. func (st *state) composed(ctx context.Context, v *delivery.Verdict) { + if st.c.Composition != nil && st.c.Try == nil { + // A carry whose candidate's fingerprints did not change keeps the composition it passed (ADR 0548 §5). + kept := *st.c.Composition + v.Composition = &kept + st.c.note("the composition is kept: no fingerprint of a module it composed changed") + return + } if st.j.Compositions == nil || st.c.Try != nil { return } @@ -243,6 +250,9 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) { return } withComposition(v, comp) + if v.Composition != nil && v.Composition.State != delivery.CompositionNotRun { + v.Composition.FPSet = st.c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5) + } } // outcome fills the verdict's levels, times, machines and composition from what ran.