DLV-C2, DLV-C6 (verdict): the checker says each pull request's verdict on its stream asks, with the out-of-step files its steps named
Design 26 has the indexer learn out-of-step generated files from the check's verdict event, so it runs no generator; the test step's answer carries them. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -910,6 +910,36 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing.
|
||||
if got[check.SDKModulePath] != "v0.1.15-feat-x.1" {
|
||||
t.Errorf("mesh-delivery was built against %v, want mesh-sdk's Go module at the group's tagged version v0.1.15-feat-x.1", got)
|
||||
}
|
||||
|
||||
// DLV-C2, DLV-C6 (design 26 *Out of step*, ADR 0620): each check's end says one verdict per pull request on the stream
|
||||
// asks, at the definition's subject and in its body (decoded strictly): the lone pull request's, then the group's two.
|
||||
verdicts, err := b.js.OrderedConsumer(b.ctx, "ASKS", jetstream.OrderedConsumerConfig{FilterSubjects: []string{meshdeliverychecker.HolderSubjectVerdict(delivery.CheckerModule)}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
said := map[int64]meshdeliverychecker.VerdictHolderEvent{}
|
||||
for len(said) < 3 {
|
||||
msg, err := verdicts.Next(jetstream.FetchMaxWait(20 * time.Second))
|
||||
if err != nil {
|
||||
t.Fatalf("verdicts said %+v, then none: %v", said, err)
|
||||
}
|
||||
var e meshdeliverychecker.VerdictHolderEvent
|
||||
dec := json.NewDecoder(bytes.NewReader(msg.Data()))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&e); err != nil {
|
||||
t.Fatalf("verdict %s is not the definition's body: %v", msg.Data(), err)
|
||||
}
|
||||
if _, twice := said[e.Pull]; twice {
|
||||
t.Errorf("a second verdict of pull %d: %+v", e.Pull, e)
|
||||
}
|
||||
said[e.Pull] = e
|
||||
}
|
||||
for pull, want := range map[int64]meshdeliverychecker.VerdictHolderEvent{7: {Repository: repo, Pull: 7, Head: head1, Verdict: delivery.VerdictPass},
|
||||
8: {Repository: repo, Pull: 8, Head: head1, Verdict: delivery.VerdictPass}, 12: {Repository: sdkRepo, Pull: 12, Head: head2, Verdict: delivery.VerdictPass}} {
|
||||
if got := said[pull]; got.Repository != want.Repository || got.Head != want.Head || got.Verdict != want.Verdict || len(got.OutOfStep) != 0 {
|
||||
t.Errorf("verdict of pull %d = %+v, want %+v", pull, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// triesStore is the tries bucket as the tools write it.
|
||||
|
||||
@@ -218,6 +218,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// registry seats' published heard on the checker's durable consumer of the shared events stream. Where that consumer is
|
||||
// not there yet a group's check asks and waits in vain until its bound, said in its verdict.
|
||||
tags := tagAsks{js: js, node: cred.Node, heard: &check.PublishedHeard{}, logf: logger.Printf, now: time.Now}
|
||||
ch.Says = tags // a pull request's verdict event, on the same stream asks (design 26 *Out of step*)
|
||||
switch cons, err := cred.EventConsumer(ctx, js, process.EventsStream); {
|
||||
case err != nil:
|
||||
logger.Printf("the consumer %s_%s on %s does not exist yet, so no package registry's published is heard and a group's check fails at its publish bound until the checker starts again: %v",
|
||||
|
||||
@@ -2,8 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
@@ -23,7 +25,8 @@ var publishedSeats = map[string]string{
|
||||
npmpackageregistry.SubjectPublished: npmpackageregistry.Seat,
|
||||
}
|
||||
|
||||
// tagAsks is check.TagAsks on the bus: tag-wanted published on the checker's own stream asks, and the published heard.
|
||||
// tagAsks is check.TagAsks and check.VerdictSayer on the bus: tag-wanted and verdict published on the checker's own stream
|
||||
// asks, and the published heard.
|
||||
type tagAsks struct {
|
||||
js jetstream.JetStream
|
||||
node string
|
||||
@@ -52,6 +55,25 @@ func (t tagAsks) Want(ctx context.Context, w check.TagWanted) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Say publishes one verdict on the checker's own stream asks with the checker's event headers; the message id makes a
|
||||
// second publish of the same pull request's head, verdict and out-of-step files within the duplicate window one event.
|
||||
func (t tagAsks) Say(ctx context.Context, v check.VerdictEvent) error {
|
||||
body, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg := nats.NewMsg(eventSubject(check.EventVerdict))
|
||||
id := fmt.Sprintf("verdict.%s.%d.%s.%s.%x", v.Repository, v.Pull, v.Head, v.Verdict, sha256.Sum256([]byte(strings.Join(v.OutOfStep, "\n"))))
|
||||
msg.Header.Set("x-event-id", id)
|
||||
msg.Header.Set("x-source", delivery.CheckerModule)
|
||||
msg.Header.Set("x-node", t.node)
|
||||
msg.Header.Set("x-time", t.now().UTC().Format(time.RFC3339Nano))
|
||||
msg.Header.Set("content-type", "application/json")
|
||||
msg.Data = body
|
||||
_, err = t.js.PublishMsg(ctx, msg, jetstream.WithMsgID(id))
|
||||
return err
|
||||
}
|
||||
|
||||
func (t tagAsks) Await(ctx context.Context, seat, head string, names []string) (check.TaggedPublished, bool) {
|
||||
return t.heard.Await(ctx, seat, head, names)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ type Outcome struct {
|
||||
// Beside are the siblings the check read at their trunk's head, by repository (ADR 0548 §6): the verdict names them, and
|
||||
// a move of one is carried.
|
||||
Beside map[string]string
|
||||
// OutOfStep are, by pull request (pullKey), the generated files out of step with their seat definitions that its steps
|
||||
// named (design 26): the verdict event carries them.
|
||||
OutOfStep map[string][]string
|
||||
}
|
||||
|
||||
// BuildNote is the state of a candidate's module build, by module: built, failed or waiting (ADR 0484 clause 7).
|
||||
@@ -90,8 +93,10 @@ type Checker struct {
|
||||
Link string
|
||||
// Announce says a status was written (EventOwedWritten); nil says nothing.
|
||||
Announce Announcer
|
||||
Now func() time.Time
|
||||
Logf func(format string, args ...any)
|
||||
// Says publishes the verdict event of each pull request whose check ended (EventVerdict); nil says none.
|
||||
Says VerdictSayer
|
||||
Now func() time.Time
|
||||
Logf func(format string, args ...any)
|
||||
// Settle is how long a group's heads must stay the same after the candidate last changed them before its check starts
|
||||
// (issue 1890): the members of a group are announced one after another, and a check of each partial set is thrown
|
||||
// away at the next. None when 0; DefaultSettle in the module.
|
||||
@@ -888,6 +893,7 @@ func (ch *Checker) try(ctx, run context.Context, key string, cs *Case) {
|
||||
return
|
||||
}
|
||||
ch.writeOwed(ctx, cs, v, out)
|
||||
ch.sayVerdict(ctx, cs, v, out)
|
||||
ch.logf("%s: %s", cs.Key, v.State)
|
||||
}
|
||||
|
||||
|
||||
@@ -430,9 +430,12 @@ func TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure(t *testing.T
|
||||
raw, _, _, _ := r.owed.Get(ctx, key)
|
||||
o, _ := delivery.DecodeCheckerOwed(raw)
|
||||
seen = append(seen, o.State)
|
||||
return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "m/a.TestX"}}}, nil
|
||||
return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "m/a.TestX"}},
|
||||
OutOfStep: map[string][]string{repoA + "#7": {"go/seats/x/contract.go"}}}, nil
|
||||
}}
|
||||
r = newRig(t, j)
|
||||
says := &verdictsSaid{}
|
||||
r.ch.Says = says
|
||||
r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1)))
|
||||
r.ch.Wait()
|
||||
key, _ := delivery.CheckerOwedKey(repoA, 7, head1)
|
||||
@@ -444,6 +447,25 @@ func TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure(t *testing.T
|
||||
if len(seen) != 1 || seen[0] != "pending" || o.State != "failure" || o.Context != delivery.StatusContextCheck || !strings.HasPrefix(o.Target, "https://view.invalid/checks/") {
|
||||
t.Errorf("while running %v, at the end %+v", seen, o)
|
||||
}
|
||||
// DLV-C2 (design 26 *Out of step*): the check's end says one verdict event for the pull request, naming its out-of-step
|
||||
// files; the running status says none.
|
||||
if len(says.said) != 1 || says.said[0].Repository != repoA || says.said[0].Pull != 7 || says.said[0].Head != head1 ||
|
||||
says.said[0].Verdict != delivery.VerdictFail || strings.Join(says.said[0].OutOfStep, ",") != "go/seats/x/contract.go" {
|
||||
t.Errorf("verdict events %+v", says.said)
|
||||
}
|
||||
}
|
||||
|
||||
// verdictsSaid is a VerdictSayer that keeps what it was told.
|
||||
type verdictsSaid struct {
|
||||
mu sync.Mutex
|
||||
said []VerdictEvent
|
||||
}
|
||||
|
||||
func (s *verdictsSaid) Say(_ context.Context, v VerdictEvent) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.said = append(s.said, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// No check of heads no candidate holds is left running (issue 1890): at start the sweep ends a key left running; while the
|
||||
|
||||
@@ -55,6 +55,19 @@ func TestTwoFailingTestsInTwoPackagesAreTwoEntries(t *testing.T) {
|
||||
t.Errorf("%s: the verdict is not written: %v", name, err)
|
||||
}
|
||||
}
|
||||
// Design 26 *Out of step* (DLV-C2): the out-of-step files the compare step names, in each result, are the outcome's for
|
||||
// the pull request, each once and sorted; a level naming none leaves the outcome with none.
|
||||
step := func(files ...string) json.RawMessage {
|
||||
raw, _ := json.Marshal(Tested{Level: delivery.LevelUnit, Tests: 1, Passed: false, Why: "out of step", OutOfStep: files})
|
||||
return raw
|
||||
}
|
||||
_, st := judgeLevel(t, step("go/seats/b/contract.go", "go/seats/a/contract.go"), step("go/seats/a/contract.go"))
|
||||
if got := st.outcome(delivery.Verdict{State: delivery.VerdictFail}).OutOfStep["novox/x#0"]; strings.Join(got, ",") != "go/seats/a/contract.go,go/seats/b/contract.go" {
|
||||
t.Errorf("out of step %v", got)
|
||||
}
|
||||
if _, st := judgeLevel(t, failingResult(t, 2, a)); st.outcome(delivery.Verdict{State: delivery.VerdictFail}).OutOfStep != nil {
|
||||
t.Errorf("a level naming no out-of-step file has %v", st.outOfStep)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1921: past delivery.MaxFailures the list names the first fifty and how many more, the status line says how many
|
||||
|
||||
@@ -257,6 +257,8 @@ type state struct {
|
||||
unpreviewed []string
|
||||
// tags are the group's tagged versions its members are built against (ADR 0607 §3), once the check heard them.
|
||||
tags groupVersions
|
||||
// outOfStep are, by pull request (pullKey), the generated files a level's compare step found out of step (design 26).
|
||||
outOfStep map[string][]string
|
||||
}
|
||||
|
||||
// Judge runs the case (it is the Judge of a Checker). A failure of the change, of a merge or of a step is a verdict; an error is only a case that cannot
|
||||
@@ -353,7 +355,7 @@ func (st *state) outcome(v delivery.Verdict) Outcome {
|
||||
slow = append(slow, fmt.Sprintf("%s %s %.1fs", level, path.Base(s.Package)+"."+s.Test, s.Elapsed))
|
||||
}
|
||||
}
|
||||
out := Outcome{Verdict: v, Slowest: slow}
|
||||
out := Outcome{Verdict: v, Slowest: slow, OutOfStep: st.outOfStep}
|
||||
if len(st.beside) > 0 {
|
||||
out.Beside = st.beside
|
||||
}
|
||||
@@ -368,6 +370,20 @@ func (st *state) outcome(v delivery.Verdict) Outcome {
|
||||
return out
|
||||
}
|
||||
|
||||
// stepOutOfStep keeps the out-of-step files a step of the pull request named, each once, sorted.
|
||||
func (st *state) stepOutOfStep(p Pull, files []string) {
|
||||
if len(files) == 0 {
|
||||
return
|
||||
}
|
||||
if st.outOfStep == nil {
|
||||
st.outOfStep = map[string][]string{}
|
||||
}
|
||||
k := pullKey(Pull{Repository: p.Repository, Number: p.Number})
|
||||
all := append(st.outOfStep[k], files...)
|
||||
slices.Sort(all)
|
||||
st.outOfStep[k] = slices.Compact(all)
|
||||
}
|
||||
|
||||
// fail is a verdict of failure at a machine.
|
||||
func fail(machine, words string) *delivery.Verdict {
|
||||
v := failure(machine, words)
|
||||
@@ -712,6 +728,7 @@ func (st *state) level(ctx context.Context, t *target, la delivery.LevelAnswer)
|
||||
if !res.Passed {
|
||||
failedResults = append(failedResults, res)
|
||||
}
|
||||
st.stepOutOfStep(t.pull, res.OutOfStep)
|
||||
}
|
||||
if len(failedResults) > 0 {
|
||||
return levelFailure(t, level, failedResults...), elapsed
|
||||
|
||||
@@ -3,8 +3,8 @@ package check
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -110,6 +110,10 @@ type Tested struct {
|
||||
// By is the build module that answered and the SDK it was built with (buildstep.By); absent from a build module from
|
||||
// before the failure contract.
|
||||
By string `json:"by,omitempty"`
|
||||
// OutOfStep are the generated files the step's regenerate-and-compare found out of step with their seat definitions,
|
||||
// each a repository-relative path (design 26; the build modules' compare step, mesh-catalog CAT-BG5 and CAT-BN2):
|
||||
// the checker names them in its verdict event. Absent from a build module that does not compare.
|
||||
OutOfStep []string `json:"out-of-step,omitempty"`
|
||||
}
|
||||
|
||||
// Digested is the answer of the Go build module's digest step: one digest per package that has tests at the level, by import
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// EventVerdict is the checker's event that says a pull request's check ended, with the generated files its steps found out
|
||||
// of step with their seat definitions (design 26 *Out of step*; ADR 0620): on the checker's own family asks (its stream
|
||||
// asks), so the indexer learns them from the event and runs no generator, and a waiter on the stream checks does not wake.
|
||||
const EventVerdict = meshdeliverychecker.HolderEventVerdict
|
||||
|
||||
// VerdictEvent is the body of verdict, as the seat definition of mesh-delivery-checker says it.
|
||||
type VerdictEvent = meshdeliverychecker.VerdictHolderEvent
|
||||
|
||||
// VerdictSayer publishes a verdict event.
|
||||
type VerdictSayer interface {
|
||||
Say(ctx context.Context, v VerdictEvent) error
|
||||
}
|
||||
|
||||
// sayVerdict says, for each pull request of a check that ended in pass or fail, one verdict event naming its head and its
|
||||
// out-of-step files. A try owes no pull request anything and says none (ADR 0501 §5). A publish that fails is said in the
|
||||
// log: the verdict and the status are written already, and the next check of the heads says it again.
|
||||
func (ch *Checker) sayVerdict(ctx context.Context, cs *Case, v delivery.Verdict, out Outcome) {
|
||||
if ch.Says == nil || cs.Try != nil || (v.State != delivery.VerdictPass && v.State != delivery.VerdictFail) {
|
||||
return
|
||||
}
|
||||
for _, p := range cs.Pulls {
|
||||
e := VerdictEvent{Repository: p.Repository, Pull: int64(p.Number), Head: p.Head, Verdict: v.State,
|
||||
OutOfStep: out.OutOfStep[pullKey(Pull{Repository: p.Repository, Number: p.Number})]}
|
||||
if err := ch.Says.Say(ctx, e); err != nil {
|
||||
ch.logf("%s: the verdict of %s#%d was written, and its event could not be said: %v", cs.Key, p.Repository, p.Number, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user