diff --git a/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go b/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go index ff6aeec3..3018f2a3 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/json" + "io/fs" "os" "path/filepath" "reflect" @@ -138,7 +139,39 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) { a.Verdict.Failures[1].Words != "novox/x#3 has files in conflict: go.mod" || a.Verdict.Machines[0] != "laptop" || a.Verdict.Levels[buildstep.LevelUnit].Run != 3 { t.Errorf("%s: the verdict %+v, want %+v", name, a.Verdict, want) } - } // Review round 1 of milestone 47/B0: the module graph's cycle rule is the program's (ADR 0603 §2, ADR 0656 §5), over the + } + // Review finding 3 of milestone 47/G: the heads of a real group carry every seat definition the reader holds, unchanged + // (this repository's own contracts, laid as its head): the answer leaves them all to the checker's list and stays + // within the bound a run's answer is written under (buildstep.RunInputBound), where the whole list would not. + t.Run("heads carrying every definition the reader holds unchanged answer none of them, within the bound", func(t *testing.T) { + real := under{prefix: "novox/mesh-delivery", fs: os.DirFS("../../../..")} + group := rules.Facts{V: rules.FactsVersion, Members: []rules.Member{{Repository: "novox/mesh-delivery", Head: "h"}}} + whole, err := rules.SeatsOf(real, group) + if err != nil || len(whole.Seats) < 10 { + t.Fatalf("the repository's own definitions: %d, %v", len(whole.Seats), err) + } + group.SeatDigests = map[string]string{} + for _, raw := range whole.Seats { + var d struct{ Seat string } + _ = json.Unmarshal(raw, &d) + group.SeatDigests[d.Seat] = rules.SeatDigest(raw) + } + same, err := rules.SeatsOf(real, group) + answer, _ := json.Marshal(same) + if err != nil || len(same.Seats) != 0 || same.Same != len(whole.Seats) || len(answer) > buildstep.RunInputBound { + t.Errorf("unchanged heads answer %d definitions (%d the same), %d bytes; want none, all %d the same, within %d: %v", + len(same.Seats), same.Same, len(answer), len(whole.Seats), buildstep.RunInputBound, err) + } + // One definition the heads change is answered alone. + for seat := range group.SeatDigests { + group.SeatDigests[seat] = "changed" + break + } + if one, err := rules.SeatsOf(real, group); err != nil || len(one.Seats) != 1 { + t.Errorf("one changed definition: answered %d, %v", len(one.Seats), err) + } + }) + // Review round 1 of milestone 47/B0: the module graph's cycle rule is the program's (ADR 0603 §2, ADR 0656 §5), over the // edges the running checker gathers. A cycle the change makes fails; one the trunk has among the same modules is noted. web, mail := rules.Edge{From: "web", To: "mail", Kind: rules.EdgeAwaits}, rules.Edge{From: "mail", To: "web", Kind: rules.EdgeAwaits} only := func(e rules.Edge, where string) rules.Edge { e.Only = where; return e } @@ -165,3 +198,19 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) { } } } + +// under is a directory laid at a repository's path of the heads, as the build node lays a head. +type under struct { + prefix string + fs fs.FS +} + +func (u under) Open(name string) (fs.File, error) { + if name == u.prefix { + return u.fs.Open(".") + } + if rest, ok := strings.CutPrefix(name, u.prefix+"/"); ok { + return u.fs.Open(rest) + } + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} +} diff --git a/modules/mesh-delivery-checker/internal/check/programs.go b/modules/mesh-delivery-checker/internal/check/programs.go index 39451929..34710fa1 100644 --- a/modules/mesh-delivery-checker/internal/check/programs.go +++ b/modules/mesh-delivery-checker/internal/check/programs.go @@ -189,7 +189,21 @@ func (st *state) seatList(ctx context.Context) bool { } base = defs } - body, err := json.Marshal(st.factsHead()) + // The program answers only the heads' definitions the list does not hold as they are: the list's digests ride in its + // input, never the list, whose definitions are larger than a run's input or answer may be (review finding 3 of G). + head := st.factsHead() + if len(base) > 0 { + head.SeatDigests = map[string]string{} + for _, raw := range base { + var d struct { + Seat string `json:"seat"` + } + if json.Unmarshal(raw, &d) == nil && d.Seat != "" { + head.SeatDigests[d.Seat] = rules.SeatDigest(raw) + } + } + } + body, err := json.Marshal(head) if err == nil { var out json.RawMessage if out, _, err = st.runProgram(ctx, []string{"-seats"}, body); err == nil { @@ -200,7 +214,7 @@ func (st *state) seatList(ctx context.Context) bool { if err == nil { st.c.Seats = overSeats(base, a.Seats) if len(a.Seats) > 0 { - st.c.note(fmt.Sprintf("the heads' seat list holds %d seat definitions, laid over the %d the seat list holds", len(a.Seats), len(base))) + st.c.note(fmt.Sprintf("the heads carry %d seat definitions new or changed (and %d the seat list holds as they are), laid over the %d the seat list holds", len(a.Seats), a.Same, len(base))) } for _, l := range a.Left { st.c.note("the heads' seat list leaves out " + l.Path + ": " + l.Why) diff --git a/modules/mesh-delivery-checker/rules/facts.go b/modules/mesh-delivery-checker/rules/facts.go index 50df21b8..1a5d7ec4 100644 --- a/modules/mesh-delivery-checker/rules/facts.go +++ b/modules/mesh-delivery-checker/rules/facts.go @@ -32,6 +32,11 @@ type Facts struct { Heads string `json:"heads"` Trunk map[string]string `json:"trunk,omitempty"` Members []Member `json:"members"` + // SeatDigests are, by seat, the digests (SeatDigest) of the definitions the running checker's reader holds: the + // `-seats` answer leaves out every head definition that is the same (milestone 47/G, the lead's ruling on review + // finding 3: a whole list is over a run answer's bound). None from a checker that hands none: then every head + // definition is answered. + SeatDigests map[string]string `json:"seat-digests,omitempty"` // Series are the changes of the members' modules' series, as structures, put in the verdict whatever it is (DLV-C4). Series []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem `json:"series,omitempty"` // Steps are, in the order met, each level a build module answered and each failure the asking met. diff --git a/modules/mesh-delivery-checker/rules/seats.go b/modules/mesh-delivery-checker/rules/seats.go index 57bea785..7c590712 100644 --- a/modules/mesh-delivery-checker/rules/seats.go +++ b/modules/mesh-delivery-checker/rules/seats.go @@ -1,7 +1,10 @@ package rules import ( + "bytes" "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -25,20 +28,38 @@ import ( const SeatsVersion = 1 // SeatsAnswer is the checking program's `-seats` answer: every seat definition the heads carry, resolved, as the requests' -// field `seats` carries them, sorted by seat; and each definition left out, with why. +// field `seats` carries them, sorted by seat, that is new or differs from the reader's (Facts.SeatDigests); how many +// were the same and left out of it (Same); and each definition left out, with why. type SeatsAnswer struct { V int `json:"v"` Seats []json.RawMessage `json:"seats"` + Same int `json:"same,omitempty"` Left []LeftOut `json:"left,omitempty"` } +// SeatDigest is a definition's digest, the same for the same definition whatever its bytes' order and spacing: the sha256 +// of its JSON with every object's keys sorted, numbers as written. Bytes that are not JSON are digested as given. +func SeatDigest(raw json.RawMessage) string { + d := json.NewDecoder(bytes.NewReader(raw)) + d.UseNumber() + var v any + canonical := []byte(raw) + if d.Decode(&v) == nil { + if out, err := json.Marshal(v); err == nil { + canonical = out + } + } + sum := sha256.Sum256(canonical) + return hex.EncodeToString(sum[:]) +} + // LeftOut is a seat definition the list does not take from the heads: its file and why. type LeftOut struct { Path string `json:"path"` Why string `json:"why"` } -// SeatsOf is the seat definitions of the facts' members' heads. heads holds each member's head at its repository's path; a contracts module is a manifest there whose package artifact names +// SeatsOf is the seat definitions of the facts' members' heads that are new or differ from the reader's (Facts.SeatDigests). heads holds each member's head at its repository's path; a contracts module is a manifest there whose package artifact names // `generated-from` (seatdefs.Of). A seat of the mesh's own a module that is not its domain's defines (seatdefs.Refusal), // and a seat two modules define where neither hands it over, are left out, said; a definition that does not parse is an // error. @@ -111,6 +132,10 @@ func SeatsOf(heads fs.FS, f Facts) (SeatsAnswer, error) { } out.Seats = make([]json.RawMessage, 0, len(by)) for _, seat := range slices.Sorted(maps.Keys(by)) { + if held, ok := f.SeatDigests[seat]; ok && held == SeatDigest(by[seat].raw) { + out.Same++ // the reader holds it as the heads carry it: the checker's own list keeps it + continue + } out.Seats = append(out.Seats, by[seat].raw) } return out, nil