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 3018f2a3..e1ca2d3e 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 @@ -14,6 +14,7 @@ import ( "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/sdk/buildstep" + "git.novox.be/novox/mesh-sdk/go/seatdef" ) // 1921 (B0, D2): the checking program answers, on its last line, the verdict the rules give the facts in its input, and @@ -146,7 +147,8 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) { 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) + // An empty list of digests answers every definition whole (a checker that hands its list's digests, holding none). + whole, err := rules.SeatsOf(real, rules.Facts{V: rules.FactsVersion, Members: group.Members, SeatDigests: map[string]string{}}) if err != nil || len(whole.Seats) < 10 { t.Fatalf("the repository's own definitions: %d, %v", len(whole.Seats), err) } @@ -162,6 +164,34 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) { 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) } + // Review round 2 of G: a checker from before G (B0's, which makes G's own check) hands no digests, and the answer + // then keeps each seat's own description only, so a group's whole list stays within the bound; each still parses. + b0, err := rules.SeatsOf(real, rules.Facts{V: rules.FactsVersion, Members: group.Members}) + if err != nil || len(b0.Seats) != len(whole.Seats) { + t.Fatalf("B0-shaped facts: %d definitions, %v", len(b0.Seats), err) + } + for i, raw := range b0.Seats { + var own struct { + Description string `json:"description"` + } + _ = json.Unmarshal(whole.Seats[i], &own) + var d struct { + Description string `json:"description"` + Verbs []json.RawMessage `json:"verbs"` + Events []json.RawMessage `json:"events"` + } + _ = json.Unmarshal(raw, &d) + inner := 0 + for _, m := range append(d.Verbs, d.Events...) { + inner += strings.Count(string(m), `"description":"`) + } + if _, perr := seatdef.Parse(raw); inner > 0 || d.Description != own.Description || perr != nil { + t.Errorf("B0-shaped facts answer %s with %d inner descriptions, its own %q: %v; want only its own", raw[:min(len(raw), 80)], inner, d.Description, perr) + } + } + if answer, _ := json.Marshal(b0); len(answer) > buildstep.RunInputBound || len(answer) >= len(mustJSON(t, whole))*3/4 { + t.Errorf("B0-shaped facts answer %d bytes (whole %d), want within %d and far below the whole", len(answer), len(mustJSON(t, whole)), buildstep.RunInputBound) + } // One definition the heads change is answered alone. for seat := range group.SeatDigests { group.SeatDigests[seat] = "changed" @@ -214,3 +244,12 @@ func (u under) Open(name string) (fs.File, error) { } return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} } + +func mustJSON(t *testing.T, v any) []byte { + t.Helper() + out, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + return out +} diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index 0db44318..1c5f9146 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -1474,6 +1474,20 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { t.Fatalf("%s: the request %+v: %v", row.name, req, err) } if row.heads != "" { + // Review round 2 of G (programs.go's digest side): the checking program's -seats input carries the digest of + // every definition the reader holds, never the list, and its answer is laid over the list (below). + var digests map[string]string + b.inputs.mu.Lock() + for _, body := range b.inputs.vals { + var f rules.Facts + if json.Unmarshal(body, &f) == nil && f.SeatDigests != nil { + digests = f.SeatDigests + } + } + b.inputs.mu.Unlock() + if want := map[string]string{"a": rules.SeatDigest(json.RawMessage(`{"seat":"a"}`)), "b": rules.SeatDigest(json.RawMessage(`{"seat": "b"}`))}; !reflect.DeepEqual(digests, want) { + t.Errorf("%s: the -seats input carries the digests %v, want the reader's list's %v", row.name, digests, want) + } var got []string for _, raw := range req.Seats { var d struct { @@ -1522,21 +1536,6 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { // fakeSeatList is the seat list reader as a test gives it: two definitions once read, else the reader's words. type fakeSeatList struct{ read bool } -// headTrees are the forge's listings of a directory at a commit, by " ". -type headTrees map[string]map[string]string - -func (h headTrees) Files(_ context.Context, repository, commit string, dirs []string) (map[string]map[string]string, error) { - out := map[string]map[string]string{} - for _, d := range dirs { - files, ok := h[repository+" "+d+" "+commit] - if !ok { - return nil, ErrNotFound - } - out[d] = files - } - return out, nil -} - func (f fakeSeatList) Definitions() ([]json.RawMessage, error) { if !f.read { return nil, errors.New("the seat list is held from the bootstrap (2 seats) and nothing new is acted on") diff --git a/modules/mesh-delivery-checker/rules/seats.go b/modules/mesh-delivery-checker/rules/seats.go index 7c590712..6c28b0b0 100644 --- a/modules/mesh-delivery-checker/rules/seats.go +++ b/modules/mesh-delivery-checker/rules/seats.go @@ -136,7 +136,14 @@ func SeatsOf(heads fs.FS, f Facts) (SeatsAnswer, error) { 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) + raw := by[seat].raw + if f.SeatDigests == nil { + // A checker from before G hands no digests, so every definition is answered: whole they are over a run answer's + // bound, so every description but the seat's own is left out, of its verbs, events and their schemas, which + // decides no verdict (neither the manifest validator nor the composer reads one); review round 2 of G. + raw = withoutInnerDescriptions(raw) + } + out.Seats = append(out.Seats, raw) } return out, nil } @@ -205,3 +212,48 @@ func localForge(head fs.FS) seatdefs.Forge { }, } } + +// schemaData are the keywords of a schema whose value is data, never a schema: what they hold is kept as written. +var schemaData = map[string]bool{"examples": true, "enum": true, "const": true, "default": true} + +// withoutInnerDescriptions is a definition with only the seat's own description kept: every other `description` string, +// of a verb, an event or anything inside their schemas, is left out, every other field kept (a property named +// description is a schema, kept; a schema's data keywords are kept whole). Bytes that are not a JSON object are given +// back as they are. +func withoutInnerDescriptions(raw json.RawMessage) json.RawMessage { + d := json.NewDecoder(bytes.NewReader(raw)) + d.UseNumber() + var def map[string]any + if d.Decode(&def) != nil { + return raw + } + var walk func(v any) + walk = func(v any) { + switch t := v.(type) { + case map[string]any: + for k, val := range t { + if _, prose := val.(string); prose && k == "description" { + delete(t, k) + continue + } + if !schemaData[k] { + walk(val) + } + } + case []any: + for _, e := range t { + walk(e) + } + } + } + for k, val := range def { + if k != "description" { + walk(val) + } + } + out, err := json.Marshal(def) + if err != nil { + return raw + } + return out +}