diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 9338a153..78b9c19a 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -9,6 +9,9 @@ import ( "fmt" "git.novox.be/novox/mesh-contracts/go/seats/meshbroker" stdio "git.novox.be/novox/mesh-sdk/go" + "go/ast" + "go/parser" + "go/token" "io/fs" "maps" "os" @@ -2870,6 +2873,68 @@ func TestGrants(t *testing.T) { } }) + // Issue 1966 (ADR 0595 §1, §11): the state a manifest keeps and a membership issues is read through mesh-sdk's two rules, + // catalogue.KeptStates and Membership.StateIssued or StateNames, which answer the old `state` section and a key-value + // entry asked in `requires` alike. A reader of a manifest's or a membership's `State` beside them misses the new form, + // the kind of this issue's faults; only the rules that judge or compose the old section itself read it. Read by the + // syntax tree across the repository, since the field's name alone is shared by other types. + t.Run("the state a manifest keeps or a membership issues is read through mesh-sdk's two rules", func(t *testing.T) { + judgesTheOldSection := map[string]bool{ + "modules/mesh-delivery-indexer/judge/busstate.go BusStateProblems": true, + "modules/mesh-delivery-indexer/judge/oldsections.go SectionsUsed": true, + "modules/mesh-delivery-indexer/judge/rules.go readOnlyStateProblems": true, + "modules/mesh-delivery-indexer/judge/rules.go parseManifest": true, + "modules/mesh-delivery-indexer/judge/rules.go StateProblems": true, + "modules/mesh-delivery-indexer/judge/rules.go ObjectsProblems": true, + "modules/mesh-grants/internal/grants/records.go bucketsOf": true, + } + planted := "package p\n\nimport \"git.novox.be/novox/mesh-sdk/go/catalogue\"\n\n" + + "func planted(m catalogue.Manifest) (n int) {\n\tfor range m.State {\n\t\tn++\n\t}\n\treturn n\n}\n" + if got, err := oldStateReaders("p.go", []byte(planted)); err != nil || !slices.Equal(got, []string{"p.go planted"}) { + t.Fatalf("a planted range m.State is read as %v (%v), want it found", got, err) + } + files, seen := 0, map[string]bool{} + err := filepath.WalkDir(filepath.Join("..", ".."), func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() && path != filepath.Join("..", "..") && (d.Name() == "testdata" || d.Name() == "node_modules" || (len(d.Name()) > 1 && d.Name()[0] == '.')) { + return filepath.SkipDir + } + if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + src, err := os.ReadFile(path) + if err != nil { + return err + } + files++ + rel, _ := filepath.Rel(filepath.Join("..", ".."), path) + readers, err := oldStateReaders(filepath.ToSlash(rel), src) + if err != nil { + return err + } + for _, r := range readers { + seen[r] = true + if !judgesTheOldSection[r] { + t.Errorf("%s reads a manifest's or a membership's State beside catalogue.KeptStates and Membership.StateIssued", r) + } + } + return nil + }) + if err != nil { + t.Fatal(err) + } + if files < 100 { + t.Fatalf("read %d source files, want the repository's", files) + } + for r := range judgesTheOldSection { + if !seen[r] { + t.Errorf("%s no longer reads the old section: take it off the list", r) + } + } + }) + // 774/S29/AC1 (ADR 0549 §5, ADR 0571 §10): the memberships composed from the assignments alone, with no read of the // controller, are the ones the union of assignments and assignments gave on the same input: the fixture's (two nodes, // the bus and mod-x on a, mod-y on b, as placed on the live mesh's own shape of nats beside its modules), whose @@ -4476,3 +4541,64 @@ func channelTakes(t *testing.T, say func(oc.Condition) error) func(oc.Condition) return say(c) } } + +// oldStateReaders names, as " ", every function of the source that reads the field State of a +// catalogue.Manifest or a membership.Membership: a receiver, parameter or declared variable of either type, or a field +// named Manifest or Membership (issue 1966's source guard). +func oldStateReaders(path string, src []byte) ([]string, error) { + f, err := parser.ParseFile(token.NewFileSet(), path, src, 0) + if err != nil { + return nil, err + } + typed := func(e ast.Expr) bool { + if s, ok := e.(*ast.StarExpr); ok { + e = s.X + } + sel, ok := e.(*ast.SelectorExpr) + if !ok { + return false + } + x, ok := sel.X.(*ast.Ident) + return ok && ((x.Name == "catalogue" && sel.Sel.Name == "Manifest") || (x.Name == "membership" && sel.Sel.Name == "Membership")) + } + var out []string + for _, decl := range f.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Body == nil { + continue + } + names := map[string]bool{} + var fields []*ast.Field + if fn.Recv != nil { + fields = append(fields, fn.Recv.List...) + } + for _, fl := range append(fields, fn.Type.Params.List...) { + if typed(fl.Type) { + for _, n := range fl.Names { + names[n.Name] = true + } + } + } + reads := false + ast.Inspect(fn.Body, func(n ast.Node) bool { + if vs, ok := n.(*ast.ValueSpec); ok && vs.Type != nil && typed(vs.Type) { + for _, n := range vs.Names { + names[n.Name] = true + } + } + if s, ok := n.(*ast.SelectorExpr); ok && s.Sel.Name == "State" { + switch x := s.X.(type) { + case *ast.Ident: + reads = reads || names[x.Name] + case *ast.SelectorExpr: + reads = reads || x.Sel.Name == "Manifest" || x.Sel.Name == "Membership" + } + } + return true + }) + if reads { + out = append(out, path+" "+fn.Name.Name) + } + } + return out, nil +}