carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "go/ast"
5 "go/parser"
6 "go/token"
7 "io/fs"
8 "os"
9 "path/filepath"
10 "sort"
11 "strconv"
12 "strings"
13 "testing"
14)
15
16// The change index (control/changes.json) only moves if every writer of the
17// deployment bucket bumps it. A bump lives in the concrete store and is off
18// until EnableChangeIndex, so the way a deployment-bucket writer goes missing
19// is a new call site that opens the bucket with the plain constructor. This
20// test finds every place the module builds a store over a bucket or a
21// directory (NewS3Store, NewS3StoreIn, an S3Store literal, NewDirStore) and
22// makes each one say which kind it is for:
23//
24// - an S3 deployment bucket (apps/, accounts/, control/): the site calls
25// NewDeploymentS3Store[In], so it never appears here at all;
26// - a deployment directory (CARLOS_DEPLOYMENT_DIR) or a hand-built
27// S3Store literal over the deployment bucket: listed in
28// deploymentManualSites, and the test checks the same function calls
29// EnableChangeIndex at least once per site;
30// - anything else: listed in otherBucketOpeners with the reason.
31//
32// Keys are "path/from/module/root.go:enclosingFunc". Every entry carries the
33// number of sites it covers, and the test fails when the count found under
34// the key differs. A new open inside a listed function is therefore a
35// failure, not a silent pass.
36
37type openerEntry struct {
38 reason string
39 count int
40}
41
42// otherBucketOpeners are the sites that open a store which is not the
43// deployment bucket: through NewS3Store, NewS3StoreIn, an S3Store literal or
44// NewDirStore.
45var otherBucketOpeners = map[string]openerEntry{
46 "cmd/console/main.go:serve": {"console seed data store: the database/data bucket (seeds/), not the deployment bucket", 1},
47 "internal/host/seedwiring.go:seedDataOpener": {"this region's data bucket, where seeds/ lives", 1},
48 "internal/host/relocatepass.go:productionRelocationRuntimeInputs": {"relocation files store: the data bucket, manifests under moves/ prefixes", 1},
49 "internal/host/relocatepass.go:registerRelocationSource": {"relocation files store: the data bucket, manifests under moves/ prefixes", 1},
50 "internal/host/claimfiles.go:newProductionFilesStore": {"claim files manifests: the data bucket, moves/ prefixes", 1},
51 "internal/hostfence/writer.go:OpenRootFenceStore": {"fence writer: the fence bucket, written under its own scoped role", 1},
52 "internal/lease/execution_store.go:ExecutionStore": {"execution/build state: the hibernate and claim data bucket, keyed by host prefix", 1},
53 "internal/status/dryrun.go:DryRun": {"status dry run: a throwaway scratch directory, not a deployment", 1},
54}
55
56// deploymentManualSites are stores over the deployment (CARLOS_DEPLOYMENT_DIR
57// directory, or a hand-built S3Store literal over the bucket) that are not
58// built by NewDeploymentS3Store. Each function must call EnableChangeIndex
59// once per site.
60var deploymentManualSites = map[string]openerEntry{
61 "cli.go:deploymentReadSeam": {"CARLOS_DEPLOYMENT_DIR", 1},
62 "cli.go:openStore": {"CARLOS_DEPLOYMENT_DIR", 1},
63 "cmd/console/main.go:openStore": {"CARLOS_DEPLOYMENT_DIR", 1},
64 "internal/edge/edge.go:enableStaticFromEnv": {"CARLOS_DEPLOYMENT_DIR", 1},
65 "internal/host/agent.go:openDeployStoreEnv": {"CARLOS_DEPLOYMENT_DIR", 1},
66 "internal/ops/ops.go:openDeployStore": {"CARLOS_DEPLOYMENT_DIR", 1},
67 "internal/vet/vet.go:openStore": {"CARLOS_DEPLOYMENT_DIR", 1},
68}
69
70type openerSite struct {
71 key string // path:func
72 line int
73 what string // what was found, for the failure message
74 kind string // "s3call", "literal" or "dir"
75 // enables is how many EnableChangeIndex calls the enclosing function has.
76 enables int
77}
78
79const releaseImportPath = "github.com/carlosframework/platform/internal/release"
80
81// scanOpenerSites lists the store constructions in one parsed file.
82// inReleasePkg is true for files of package release itself, where the
83// constructors and the type are referred to without a qualifier.
84func scanOpenerSites(fset *token.FileSet, f *ast.File, rel string, inReleasePkg bool) []openerSite {
85 qual := ""
86 for _, im := range f.Imports {
87 p, _ := strconv.Unquote(im.Path.Value)
88 if p != releaseImportPath {
89 continue
90 }
91 qual = "release"
92 if im.Name != nil {
93 qual = im.Name.Name
94 }
95 }
96 if qual == "" && !inReleasePkg {
97 return nil
98 }
99 // isRelease reports whether e names the identifier in package release.
100 isRelease := func(e ast.Expr, name string) bool {
101 switch x := e.(type) {
102 case *ast.SelectorExpr:
103 id, ok := x.X.(*ast.Ident)
104 return ok && qual != "" && id.Name == qual && x.Sel.Name == name
105 case *ast.Ident:
106 return inReleasePkg && x.Name == name
107 }
108 return false
109 }
110 var sites []openerSite
111 for _, decl := range f.Decls {
112 fn := "<package>"
113 var root ast.Node = decl
114 if fd, ok := decl.(*ast.FuncDecl); ok {
115 fn = fd.Name.Name
116 if fd.Body == nil {
117 continue
118 }
119 root = fd.Body
120 }
121 enables := 0
122 var found []openerSite
123 ast.Inspect(root, func(n ast.Node) bool {
124 switch x := n.(type) {
125 case *ast.CallExpr:
126 if sel, ok := x.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "EnableChangeIndex" {
127 enables++
128 }
129 for _, name := range []string{"NewS3Store", "NewS3StoreIn"} {
130 if isRelease(x.Fun, name) {
131 found = append(found, openerSite{key: rel + ":" + fn, line: fset.Position(x.Pos()).Line, what: "call to " + name, kind: "s3call"})
132 }
133 }
134 if isRelease(x.Fun, "NewDirStore") {
135 found = append(found, openerSite{key: rel + ":" + fn, line: fset.Position(x.Pos()).Line, what: "call to NewDirStore", kind: "dir"})
136 }
137 case *ast.CompositeLit:
138 if isRelease(x.Type, "S3Store") {
139 found = append(found, openerSite{key: rel + ":" + fn, line: fset.Position(x.Pos()).Line, what: "S3Store literal", kind: "literal"})
140 }
141 }
142 return true
143 })
144 for _, s := range found {
145 s.enables = enables
146 sites = append(sites, s)
147 }
148 }
149 return sites
150}
151
152func moduleRoot(t *testing.T) string {
153 t.Helper()
154 dir, err := os.Getwd()
155 if err != nil {
156 t.Fatal(err)
157 }
158 for {
159 if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
160 return dir
161 }
162 parent := filepath.Dir(dir)
163 if parent == dir {
164 t.Fatal("no go.mod above the test directory")
165 }
166 dir = parent
167 }
168}
169
170func TestS3StoreOpenersClassified(t *testing.T) {
171 root := moduleRoot(t)
172 fset := token.NewFileSet()
173 var sites []openerSite
174 err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
175 if err != nil {
176 return err
177 }
178 name := d.Name()
179 if d.IsDir() {
180 if path != root && (strings.HasPrefix(name, ".") || name == "vendor" || name == "testdata" || name == "node_modules") {
181 return filepath.SkipDir
182 }
183 if path != root {
184 if _, err := os.Stat(filepath.Join(path, "go.mod")); err == nil {
185 return filepath.SkipDir // a nested module
186 }
187 }
188 return nil
189 }
190 if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
191 return nil
192 }
193 rel, err := filepath.Rel(root, path)
194 if err != nil {
195 return err
196 }
197 rel = filepath.ToSlash(rel)
198 if rel == "internal/release/store.go" {
199 return nil // the constructors themselves
200 }
201 f, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
202 if err != nil {
203 return err
204 }
205 sites = append(sites, scanOpenerSites(fset, f, rel, f.Name.Name == "release" && filepath.ToSlash(filepath.Dir(rel)) == "internal/release")...)
206 return nil
207 })
208 if err != nil {
209 t.Fatal(err)
210 }
211
212 byKey := map[string][]openerSite{}
213 for _, s := range sites {
214 byKey[s.key] = append(byKey[s.key], s)
215 }
216 var bad []string
217 for key, ss := range byKey {
218 if e, ok := deploymentManualSites[key]; ok {
219 for _, s := range ss {
220 if s.kind == "s3call" {
221 bad = append(bad, key+" (line "+strconv.Itoa(s.line)+"): "+s.what+" in a deployment site; use NewDeploymentS3Store[In]")
222 }
223 }
224 if len(ss) != e.count {
225 bad = append(bad, key+": "+strconv.Itoa(len(ss))+" store opens found, deploymentManualSites says "+strconv.Itoa(e.count)+" ("+e.reason+"); classify the new one and update the count")
226 }
227 if ss[0].enables < len(ss) {
228 bad = append(bad, key+": "+strconv.Itoa(len(ss))+" deployment store opens but only "+strconv.Itoa(ss[0].enables)+" EnableChangeIndex calls in the function")
229 }
230 continue
231 }
232 if e, ok := otherBucketOpeners[key]; ok {
233 if len(ss) != e.count {
234 bad = append(bad, key+": "+strconv.Itoa(len(ss))+" store opens found, otherBucketOpeners says "+strconv.Itoa(e.count)+" ("+e.reason+"); classify the new one and update the count")
235 }
236 continue
237 }
238 for _, s := range ss {
239 bad = append(bad, key+" (line "+strconv.Itoa(s.line)+"): "+s.what+" is not classified. S3 deployment bucket: use NewDeploymentS3Store[In]. Deployment directory or literal: call EnableChangeIndex and list it in deploymentManualSites. Anything else: add it to otherBucketOpeners with the reason")
240 }
241 }
242 for k := range otherBucketOpeners {
243 if byKey[k] == nil {
244 bad = append(bad, k+": listed in otherBucketOpeners but no longer opens a store (remove the entry)")
245 }
246 }
247 for k := range deploymentManualSites {
248 if byKey[k] == nil {
249 bad = append(bad, k+": listed in deploymentManualSites but no longer opens a store (remove the entry)")
250 }
251 }
252 sort.Strings(bad)
253 for _, b := range bad {
254 t.Error(b)
255 }
256 if len(sites) == 0 {
257 t.Fatal("the scan found no store sites at all; the walk is not reaching the module")
258 }
259}
260
261// TestOpenerScanRecognisesEveryForm keeps the scanner honest: qualified and
262// aliased constructors, literals with and without &, and the enable check.
263func TestOpenerScanRecognisesEveryForm(t *testing.T) {
264 const src = `package x
265
266import rel "github.com/carlosframework/platform/internal/release"
267
268func a() { rel.NewS3Store(nil, "b") }
269func b() { rel.NewS3StoreIn(nil, "b", "r") }
270func c() { _ = &rel.S3Store{Bucket: "b"} }
271func d() { s := rel.S3Store{Bucket: "b"}; _ = s }
272func e() { s := &rel.S3Store{Bucket: "b"}; s.EnableChangeIndex() }
273func f() { rel.NewDeploymentS3Store(nil, "b") }
274func g() { rel.NewDirStore("d") }
275func h() { d := rel.NewDirStore("d"); d.EnableChangeIndex(); _ = rel.NewDirStore("e") }
276`
277 fset := token.NewFileSet()
278 f, err := parser.ParseFile(fset, "x.go", src, 0)
279 if err != nil {
280 t.Fatal(err)
281 }
282 got := map[string][]openerSite{}
283 for _, s := range scanOpenerSites(fset, f, "x.go", false) {
284 got[s.key] = append(got[s.key], s)
285 }
286 for _, want := range []string{"x.go:a", "x.go:b", "x.go:c", "x.go:d", "x.go:e", "x.go:g"} {
287 if len(got[want]) != 1 {
288 t.Errorf("%s: %d sites, want 1", want, len(got[want]))
289 }
290 }
291 if len(got["x.go:f"]) != 0 {
292 t.Error("NewDeploymentS3Store is not a plain opener")
293 }
294 if len(got["x.go:h"]) != 2 || got["x.go:h"][0].enables != 1 {
295 t.Errorf("h: want two dir sites and one enable call, got %+v", got["x.go:h"])
296 }
297 if got["x.go:c"][0].kind != "literal" || got["x.go:c"][0].enables != 0 {
298 t.Errorf("c: literal without enable, got %+v", got["x.go:c"])
299 }
300 if got["x.go:g"][0].kind != "dir" || got["x.go:e"][0].enables != 1 {
301 t.Errorf("g/e: kinds wrong, got %+v %+v", got["x.go:g"], got["x.go:e"])
302 }
303}
304