| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "go/ast" |
| 6 | "go/parser" |
| 7 | "go/token" |
| 8 | "io/fs" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "regexp" |
| 12 | "sort" |
| 13 | "strconv" |
| 14 | "strings" |
| 15 | "testing" |
| 16 | ) |
| 17 | |
| 18 | // Moves into the home region, part 2, Task 3: the static half of Review |
| 19 | // Focus 1 (supplied regions are @home). This is a pin, not a proof. It |
| 20 | // lists every production call site that hands the claim a region on a |
| 21 | // move's path, with the argument expression text a reviewer saw and the |
| 22 | // runtime witness that drives the site with a home target from clean claim |
| 23 | // state, or "named-only" with the guard that keeps a home target away. The |
| 24 | // proof is the runtime one (TestHomeTargetFreshStateShowsOnlyTheHomeToken |
| 25 | // in internal/host, where a wrong spelling cannot hide). This test |
| 26 | // classifies nothing as safe by its shape: a new site, an edited argument |
| 27 | // or a vanished site fails until a reviewer updates the row and names a |
| 28 | // witness. |
| 29 | |
| 30 | // homeRegionSite is one inventoried site. Callee is the call with the |
| 31 | // region parameter in parentheses, the composite type with the field in |
| 32 | // braces, or the assigned field; Arg is the argument expression as |
| 33 | // go/printer prints it. |
| 34 | type homeRegionSite struct { |
| 35 | File, Func, Callee, Arg string |
| 36 | // Witness is the runtime test that drives the site with a home target |
| 37 | // from clean claim state, or "named-only: <the guard>". |
| 38 | Witness string |
| 39 | // Pending names the task that writes Witness while it is not written |
| 40 | // yet; the test logs the row instead of requiring the test. |
| 41 | Pending string |
| 42 | // Note says why the row's argument is what it is, where that is not |
| 43 | // the placement-derived @home a reviewer would expect. |
| 44 | Note string |
| 45 | } |
| 46 | |
| 47 | // The witnesses. |
| 48 | const ( |
| 49 | wFresh = "TestHomeTargetFreshStateShowsOnlyTheHomeToken" |
| 50 | wBegin = "TestPlan13BeginClaimMoveHomeAndNamed" |
| 51 | wConsoleCommt = "TestMoveRecoveryCommitIntoHome" |
| 52 | wRevert = "TestMoveIntoHomePreFlipRevert" |
| 53 | wAbort = "TestMoveIntoHomeAbortParksTheHomeLane" |
| 54 | wClean = "TestMoveIntoHomeCleanRollback" |
| 55 | wDirty = "TestMoveIntoHomeDirtyReturn" |
| 56 | wFenced = "TestMoveIntoHomeFencedTargetRelease" |
| 57 | wLost = "TestMoveIntoHomeDeclareLost" |
| 58 | lossNote = "retained, validated intent: claimLossIntent copies TargetRegion from op.intent.to (@home, written by the start CAS) and the loss record signs it; SourceRegion is the record's From through park.PlacementRegion" |
| 59 | ) |
| 60 | |
| 61 | // homeRegionCallSites is the golden inventory, sorted by file, function and |
| 62 | // callee. |
| 63 | var homeRegionCallSites = []homeRegionSite{ |
| 64 | {File: "internal/console/apiinstancemove.go", Func: "Server.handleAPIInstanceMoveRequest", Callee: "hostclaim.BeginMove(targetRegion)", Arg: "to", |
| 65 | Witness: "named-only: without host claims the door refuses a home target before the plain path's BeginMove (MV-N510, Ruling R1; TestMoveDoorPlainPathRefusesHomeTarget); a host-claim console starts through BeginClaimMove"}, |
| 66 | {File: "internal/host/movesabort.go", Func: "movesWorker.abortClaim", Callee: "hostclaim.Abort(targetRegion)", Arg: "park.PlacementRegion(rec.Move.To)", Witness: wAbort}, |
| 67 | {File: "internal/host/movescut.go", Func: "movesWorker.revertClaim", Callee: "hostclaim.Revert(targetRegion)", Arg: "park.PlacementRegion(rec.Move.To)", Witness: wRevert}, |
| 68 | {File: "internal/host/movesinstall.go", Func: "installRun.parking", Callee: "hostclaim.QueueResidueParks(region)", Arg: "cfg.parkRegion()", Witness: wFresh}, |
| 69 | {File: "internal/host/movesland.go", Func: "landingRun.checking", Callee: "hostclaim.Touch(regions)", Arg: "nil", Witness: wFresh, |
| 70 | Note: "the landing touches its own box only; the source round touched the region"}, |
| 71 | {File: "internal/host/moveslandcommit.go", Func: "landingRun.commitClaim", Callee: "hostclaim.Commit(sourceRegion)", Arg: "park.PlacementRegion(l.rec.Move.From)", Witness: wFresh}, |
| 72 | {File: "internal/host/moveslandcommit.go", Func: "landingRun.commitClaim", Callee: "hostclaim.Commit(targetRegion)", Arg: "cfg.parkRegion()", Witness: wFresh}, |
| 73 | {File: "internal/host/movespass.go", Func: "stagingLeaseKey", Callee: "moves.StagingLeasePrefix(targetRegion)", Arg: "r", Witness: wFresh, |
| 74 | Note: "r is park.PlacementRegion(targetRegion), the lane's record code"}, |
| 75 | {File: "internal/host/movesphase1.go", Func: "movesWorker.sourceRound", Callee: "hostclaim.Touch(regions)", Arg: "[]string{park.PlacementRegion(rec.Move.To)}", Witness: wFresh}, |
| 76 | {File: "internal/host/movesrecover.go", Func: "recoverRun.probed", Callee: "hostclaim.QueueReturnLanePark(sourceRegion)", Arg: "cfg.parkRegion()", Witness: wDirty, |
| 77 | Note: "runs on the named source; its region is the source's"}, |
| 78 | {File: "internal/host/movesrollback.go", Func: "rollbackRun.claimReleased", Callee: "hostclaim.TargetReleased(targetRegion)", Arg: "cfg.parkRegion()", Witness: wClean}, |
| 79 | {File: "internal/host/movesrollback.go", Func: "rollbackRun.remote", Callee: "hostclaim.QueueStagingPark(region)", Arg: "cfg.parkRegion()", Witness: wClean}, |
| 80 | {File: "internal/hostops/move_enrollment.go", Func: "BeginClaimMove", Callee: "hostclaim.BeginAdmittedMove(targetRegion)", Arg: "park.PlacementRegion(to)", Witness: wBegin}, |
| 81 | {File: "internal/hostops/move_fence_release.go", Func: "MoveRecoveryGrant.releaseFencedTarget", Callee: "hostclaim.FencedTargetRelease{SourceRegion}", Arg: "park.PlacementRegion(m.From)", Witness: wFenced}, |
| 82 | {File: "internal/hostops/move_fence_release.go", Func: "MoveRecoveryGrant.releaseFencedTarget", Callee: "hostclaim.FencedTargetRelease{TargetRegion}", Arg: "park.PlacementRegion(m.To)", Witness: wFenced}, |
| 83 | {File: "internal/hostops/move_fence_release.go", Func: "MoveRecoveryGrant.releaseFencedTarget", Callee: "hostclaim.ReleaseFencedTarget(r)", Arg: "r", Witness: wFenced}, |
| 84 | {File: "internal/hostops/move_loss_grant.go", Func: "lossScopedStore.claim", Callee: "hostclaim.ReleaseLostTarget(r)", Arg: "rel", Witness: wLost, |
| 85 | Note: "rel is moveLossRelease over the intent read back from the signed loss record (lossIntentOf), not a new derivation"}, |
| 86 | {File: "internal/hostops/move_loss_owner.go", Func: "claimLossIntent", Callee: "moveLossIntent{TargetRegion}", Arg: `op.Intent["to"]`, Witness: wLost, Note: lossNote}, |
| 87 | {File: "internal/hostops/move_loss_owner.go", Func: "moveLossOwner.FinishLoss", Callee: "hostclaim.ReleaseLostTarget(r)", Arg: "moveLossRelease(in, durable, c.Holder, loss.Host)", Witness: wLost}, |
| 88 | {File: "internal/hostops/move_loss_owner.go", Func: "moveLossRelease", Callee: "hostclaim.LostTargetRelease{SourceRegion}", Arg: "in.SourceRegion", Witness: wLost, Note: lossNote}, |
| 89 | {File: "internal/hostops/move_loss_owner.go", Func: "moveLossRelease", Callee: "hostclaim.LostTargetRelease{TargetRegion}", Arg: "in.TargetRegion", Witness: wLost, Note: lossNote}, |
| 90 | {File: "internal/hostops/move_loss_owner.go", Func: "recordLossIntent", Callee: "in.SourceRegion =", Arg: "park.PlacementRegion(m.From)", Witness: wLost, Note: lossNote}, |
| 91 | {File: "internal/hostops/move_recovery_grant.go", Func: "MoveRecoveryGrant.Claim", Callee: "hostclaim.Commit(sourceRegion)", Arg: "park.PlacementRegion(m.From)", Witness: wConsoleCommt}, |
| 92 | {File: "internal/hostops/move_recovery_grant.go", Func: "MoveRecoveryGrant.Claim", Callee: "hostclaim.Commit(targetRegion)", Arg: "park.PlacementRegion(m.To)", Witness: wConsoleCommt}, |
| 93 | } |
| 94 | |
| 95 | // homeRegionCallees are the claim transitions and the lease key that take a |
| 96 | // region, by package, with the index and name of each region parameter. |
| 97 | var homeRegionCallees = map[string]map[string][]homeRegionParam{ |
| 98 | "internal/hostclaim": { |
| 99 | "Touch": {{1, "regions"}}, |
| 100 | "Abort": {{1, "targetRegion"}}, |
| 101 | "Revert": {{2, "targetRegion"}}, |
| 102 | "Commit": {{1, "sourceRegion"}, {3, "targetRegion"}}, |
| 103 | "QueueLocationPark": {{1, "region"}}, |
| 104 | "QueueStagingPark": {{1, "region"}}, |
| 105 | "QueueResidueParks": {{1, "region"}}, |
| 106 | "QueueReturnLanePark": {{1, "sourceRegion"}}, |
| 107 | "TargetReleased": {{2, "targetRegion"}}, |
| 108 | "BeginMove": {{4, "targetRegion"}}, |
| 109 | "BeginAdmittedMove": {{4, "targetRegion"}}, |
| 110 | "ReleaseFencedTarget": {{0, "r"}}, |
| 111 | "ReleaseLostTarget": {{0, "r"}}, |
| 112 | }, |
| 113 | "internal/moves": { |
| 114 | "StagingLeasePrefix": {{0, "targetRegion"}}, |
| 115 | }, |
| 116 | } |
| 117 | |
| 118 | type homeRegionParam struct { |
| 119 | index int |
| 120 | name string |
| 121 | } |
| 122 | |
| 123 | // homeRegionLiterals are the composite types whose region fields are |
| 124 | // inventoried: the claim's two release literals, and the loss owner's own |
| 125 | // intent (claimLossIntent builds it). The empty package is a type of the |
| 126 | // scanned file's own package. |
| 127 | var homeRegionLiterals = map[string]map[string]bool{ |
| 128 | "internal/hostclaim": {"FencedTargetRelease": true, "LostTargetRelease": true}, |
| 129 | "": {"moveLossIntent": true}, |
| 130 | } |
| 131 | |
| 132 | // homeRegionFields are those literals' region fields. An assignment to one |
| 133 | // (recordLossIntent's in.SourceRegion) is a site too. |
| 134 | var homeRegionFields = map[string]bool{"TargetRegion": true, "SourceRegion": true} |
| 135 | |
| 136 | var homeRegionDirs = []string{"internal/host", "internal/hostops", "internal/console"} |
| 137 | |
| 138 | const homeRegionModule = "github.com/carlosframework/platform/" |
| 139 | |
| 140 | // scanHomeRegionSites lists the sites in every non-test Go file under the |
| 141 | // scanned directories. overlay replaces a file's source by its |
| 142 | // slash-separated path from root ("" removes the file) or adds a file. |
| 143 | func scanHomeRegionSites(root string, overlay map[string]string) ([]homeRegionSite, error) { |
| 144 | files := map[string]bool{} |
| 145 | for _, dir := range homeRegionDirs { |
| 146 | err := filepath.WalkDir(filepath.Join(root, dir), func(path string, d fs.DirEntry, err error) error { |
| 147 | if err != nil { |
| 148 | return err |
| 149 | } |
| 150 | if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { |
| 151 | return nil |
| 152 | } |
| 153 | rel, err := filepath.Rel(root, path) |
| 154 | if err != nil { |
| 155 | return err |
| 156 | } |
| 157 | files[filepath.ToSlash(rel)] = true |
| 158 | return nil |
| 159 | }) |
| 160 | if err != nil { |
| 161 | return nil, err |
| 162 | } |
| 163 | } |
| 164 | for path := range overlay { |
| 165 | files[path] = true |
| 166 | } |
| 167 | var names []string |
| 168 | for f := range files { |
| 169 | names = append(names, f) |
| 170 | } |
| 171 | sort.Strings(names) |
| 172 | var out []homeRegionSite |
| 173 | for _, name := range names { |
| 174 | var src any |
| 175 | if body, ok := overlay[name]; ok { |
| 176 | if body == "" { |
| 177 | continue |
| 178 | } |
| 179 | src = body |
| 180 | } |
| 181 | f, err := parser.ParseFile(token.NewFileSet(), filepath.Join(root, filepath.FromSlash(name)), src, parser.SkipObjectResolution) |
| 182 | if err != nil { |
| 183 | return nil, err |
| 184 | } |
| 185 | out = append(out, homeRegionFileSites(name, f)...) |
| 186 | } |
| 187 | return out, nil |
| 188 | } |
| 189 | |
| 190 | // homeRegionFileSites lists one file's sites, each under the function (or |
| 191 | // method, by receiver type) that encloses it; a function literal's sites |
| 192 | // are its enclosing declaration's. |
| 193 | func homeRegionFileSites(name string, f *ast.File) []homeRegionSite { |
| 194 | // The file's local names for the scanned packages. |
| 195 | local := map[string]string{} |
| 196 | for _, imp := range f.Imports { |
| 197 | path, _ := strconv.Unquote(imp.Path.Value) |
| 198 | rel, ok := strings.CutPrefix(path, homeRegionModule) |
| 199 | if !ok { |
| 200 | continue |
| 201 | } |
| 202 | if homeRegionCallees[rel] == nil && homeRegionLiterals[rel] == nil { |
| 203 | continue |
| 204 | } |
| 205 | n := filepath.Base(rel) |
| 206 | if imp.Name != nil { |
| 207 | n = imp.Name.Name |
| 208 | } |
| 209 | local[n] = rel |
| 210 | } |
| 211 | resolve := func(e ast.Expr) (rel, sel string, ok bool) { |
| 212 | switch x := e.(type) { |
| 213 | case *ast.SelectorExpr: |
| 214 | id, isID := x.X.(*ast.Ident) |
| 215 | if !isID { |
| 216 | return "", "", false |
| 217 | } |
| 218 | rel, ok = local[id.Name] |
| 219 | return rel, x.Sel.Name, ok |
| 220 | case *ast.Ident: |
| 221 | return "", x.Name, true |
| 222 | } |
| 223 | return "", "", false |
| 224 | } |
| 225 | var out []homeRegionSite |
| 226 | visit := func(fn string, root ast.Node) { |
| 227 | ast.Inspect(root, func(n ast.Node) bool { |
| 228 | switch x := n.(type) { |
| 229 | case *ast.CallExpr: |
| 230 | rel, callee, ok := resolve(x.Fun) |
| 231 | if !ok || rel == "" { |
| 232 | return true |
| 233 | } |
| 234 | for _, p := range homeRegionCallees[rel][callee] { |
| 235 | arg := "<missing>" |
| 236 | if p.index < len(x.Args) { |
| 237 | arg = plan13Expr(x.Args[p.index]) |
| 238 | } |
| 239 | out = append(out, homeRegionSite{File: name, Func: fn, Callee: filepath.Base(rel) + "." + callee + "(" + p.name + ")", Arg: arg}) |
| 240 | } |
| 241 | case *ast.CompositeLit: |
| 242 | rel, typ, ok := resolve(x.Type) |
| 243 | if !ok || !homeRegionLiterals[rel][typ] { |
| 244 | return true |
| 245 | } |
| 246 | if rel != "" { |
| 247 | typ = filepath.Base(rel) + "." + typ |
| 248 | } |
| 249 | for _, el := range x.Elts { |
| 250 | kv, isKV := el.(*ast.KeyValueExpr) |
| 251 | if !isKV { |
| 252 | continue |
| 253 | } |
| 254 | if key, isID := kv.Key.(*ast.Ident); isID && homeRegionFields[key.Name] { |
| 255 | out = append(out, homeRegionSite{File: name, Func: fn, Callee: typ + "{" + key.Name + "}", Arg: plan13Expr(kv.Value)}) |
| 256 | } |
| 257 | } |
| 258 | case *ast.AssignStmt: |
| 259 | for i, lhs := range x.Lhs { |
| 260 | sel, isSel := lhs.(*ast.SelectorExpr) |
| 261 | if !isSel || !homeRegionFields[sel.Sel.Name] { |
| 262 | continue |
| 263 | } |
| 264 | arg := "<multi-value>" |
| 265 | if len(x.Rhs) == len(x.Lhs) { |
| 266 | arg = plan13Expr(x.Rhs[i]) |
| 267 | } |
| 268 | out = append(out, homeRegionSite{File: name, Func: fn, Callee: plan13Expr(sel) + " =", Arg: arg}) |
| 269 | } |
| 270 | } |
| 271 | return true |
| 272 | }) |
| 273 | } |
| 274 | for _, d := range f.Decls { |
| 275 | switch x := d.(type) { |
| 276 | case *ast.FuncDecl: |
| 277 | fn := x.Name.Name |
| 278 | if x.Recv != nil && len(x.Recv.List) == 1 { |
| 279 | t := x.Recv.List[0].Type |
| 280 | if star, ok := t.(*ast.StarExpr); ok { |
| 281 | t = star.X |
| 282 | } |
| 283 | if ix, ok := t.(*ast.IndexExpr); ok { |
| 284 | t = ix.X |
| 285 | } |
| 286 | fn = plan13Expr(t) + "." + fn |
| 287 | } |
| 288 | if x.Body != nil { |
| 289 | visit(fn, x.Body) |
| 290 | } |
| 291 | case *ast.GenDecl: |
| 292 | visit("<package>", x) |
| 293 | } |
| 294 | } |
| 295 | return out |
| 296 | } |
| 297 | |
| 298 | // homeRegionDiff compares the sites found with the inventory. A site is |
| 299 | // keyed by its file, function and callee; within one key the argument |
| 300 | // texts are matched as a multiset. What is left over is a changed site |
| 301 | // (one found and one row unmatched under the same key), an unknown site (a |
| 302 | // found site with no row left), or a stale row (a row with no site left). |
| 303 | func homeRegionDiff(found, inventory []homeRegionSite) []string { |
| 304 | type key struct{ file, fn, callee string } |
| 305 | want := map[key][]string{} |
| 306 | for _, r := range inventory { |
| 307 | k := key{r.File, r.Func, r.Callee} |
| 308 | want[k] = append(want[k], r.Arg) |
| 309 | } |
| 310 | got := map[key][]string{} |
| 311 | for _, s := range found { |
| 312 | k := key{s.File, s.Func, s.Callee} |
| 313 | got[k] = append(got[k], s.Arg) |
| 314 | } |
| 315 | keys := map[key]bool{} |
| 316 | for k := range want { |
| 317 | keys[k] = true |
| 318 | } |
| 319 | for k := range got { |
| 320 | keys[k] = true |
| 321 | } |
| 322 | const fix = "; add or update its row in homeRegionCallSites (home_region_args_test.go) and name the runtime witness that drives it with a home target from clean claim state" |
| 323 | var out []string |
| 324 | for k := range keys { |
| 325 | rows := append([]string(nil), want[k]...) |
| 326 | var sites []string |
| 327 | for _, a := range got[k] { |
| 328 | if i := indexOf(rows, a); i >= 0 { |
| 329 | rows = append(rows[:i], rows[i+1:]...) |
| 330 | continue |
| 331 | } |
| 332 | sites = append(sites, a) |
| 333 | } |
| 334 | n := min(len(rows), len(sites)) |
| 335 | for i := 0; i < n; i++ { |
| 336 | out = append(out, fmt.Sprintf("changed site: %s %s %s passes %s, its row says %s%s", k.file, k.fn, k.callee, sites[i], rows[i], fix)) |
| 337 | } |
| 338 | for _, a := range sites[n:] { |
| 339 | out = append(out, fmt.Sprintf("unknown site: %s %s %s passes %s%s", k.file, k.fn, k.callee, a, fix)) |
| 340 | } |
| 341 | for _, a := range rows[n:] { |
| 342 | out = append(out, fmt.Sprintf("stale row: %s %s %s passing %s is no longer there%s", k.file, k.fn, k.callee, a, fix)) |
| 343 | } |
| 344 | } |
| 345 | sort.Strings(out) |
| 346 | return out |
| 347 | } |
| 348 | |
| 349 | func indexOf(xs []string, x string) int { |
| 350 | for i, s := range xs { |
| 351 | if s == x { |
| 352 | return i |
| 353 | } |
| 354 | } |
| 355 | return -1 |
| 356 | } |
| 357 | |
| 358 | // homeRegionTestNames is every top-level test function name in the |
| 359 | // repository's test files. |
| 360 | func homeRegionTestNames(t *testing.T) map[string]bool { |
| 361 | t.Helper() |
| 362 | re := regexp.MustCompile(`(?m)^func (Test\w+)\(t \*testing\.T\)`) |
| 363 | out := map[string]bool{} |
| 364 | err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { |
| 365 | if err != nil { |
| 366 | return err |
| 367 | } |
| 368 | if d.IsDir() { |
| 369 | if path != "." && (strings.HasPrefix(d.Name(), ".") || d.Name() == "node_modules" || d.Name() == "testdata") { |
| 370 | return filepath.SkipDir |
| 371 | } |
| 372 | return nil |
| 373 | } |
| 374 | if !strings.HasSuffix(path, "_test.go") { |
| 375 | return nil |
| 376 | } |
| 377 | body, err := os.ReadFile(path) |
| 378 | if err != nil { |
| 379 | return err |
| 380 | } |
| 381 | for _, m := range re.FindAllStringSubmatch(string(body), -1) { |
| 382 | out[m[1]] = true |
| 383 | } |
| 384 | return nil |
| 385 | }) |
| 386 | if err != nil { |
| 387 | t.Fatal(err) |
| 388 | } |
| 389 | return out |
| 390 | } |
| 391 | |
| 392 | // homeRegionOverlay is path's source with each old, new pair applied: old |
| 393 | // must be there exactly once. |
| 394 | func homeRegionOverlay(t *testing.T, path string, pairs ...string) map[string]string { |
| 395 | t.Helper() |
| 396 | raw, err := os.ReadFile(filepath.FromSlash(path)) |
| 397 | if err != nil { |
| 398 | t.Fatal(err) |
| 399 | } |
| 400 | body := string(raw) |
| 401 | for i := 0; i+1 < len(pairs); i += 2 { |
| 402 | if n := strings.Count(body, pairs[i]); n != 1 { |
| 403 | t.Fatalf("fixture: %s holds %q %d times, want once", path, pairs[i], n) |
| 404 | } |
| 405 | body = strings.Replace(body, pairs[i], pairs[i+1], 1) |
| 406 | } |
| 407 | return map[string]string{path: body} |
| 408 | } |
| 409 | |
| 410 | // homeRegionWant fails unless problems hold one starting with kind and |
| 411 | // containing every part. |
| 412 | func homeRegionWant(t *testing.T, problems []string, kind string, parts ...string) { |
| 413 | t.Helper() |
| 414 | for _, p := range problems { |
| 415 | if !strings.HasPrefix(p, kind) { |
| 416 | continue |
| 417 | } |
| 418 | all := true |
| 419 | for _, part := range parts { |
| 420 | all = all && strings.Contains(p, part) |
| 421 | } |
| 422 | if all { |
| 423 | return |
| 424 | } |
| 425 | } |
| 426 | t.Fatalf("no %q naming %q among %q", kind, parts, problems) |
| 427 | } |
| 428 | |
| 429 | func TestMovesHomeRegionCallSiteInventory(t *testing.T) { |
| 430 | found, err := scanHomeRegionSites(".", nil) |
| 431 | if err != nil { |
| 432 | t.Fatal(err) |
| 433 | } |
| 434 | |
| 435 | t.Run("the tree matches the inventory", func(t *testing.T) { |
| 436 | if problems := homeRegionDiff(found, homeRegionCallSites); len(problems) != 0 { |
| 437 | for _, s := range found { |
| 438 | t.Logf("found {File: %q, Func: %q, Callee: %q, Arg: %q}", s.File, s.Func, s.Callee, s.Arg) |
| 439 | } |
| 440 | for _, p := range problems { |
| 441 | t.Error(p) |
| 442 | } |
| 443 | } |
| 444 | }) |
| 445 | |
| 446 | t.Run("every row names a witness", func(t *testing.T) { |
| 447 | tests := homeRegionTestNames(t) |
| 448 | for _, r := range homeRegionCallSites { |
| 449 | switch { |
| 450 | case strings.HasPrefix(r.Witness, "named-only: ") && len(r.Witness) > len("named-only: "): |
| 451 | case r.Witness == "": |
| 452 | t.Errorf("%s %s %s names no witness", r.File, r.Func, r.Callee) |
| 453 | case r.Pending != "": |
| 454 | if tests[r.Witness] { |
| 455 | t.Errorf("%s %s %s: %s is written; clear the row's Pending (%s)", r.File, r.Func, r.Callee, r.Witness, r.Pending) |
| 456 | } |
| 457 | t.Logf("pending (%s): %s %s %s, witness %s", r.Pending, r.File, r.Func, r.Callee, r.Witness) |
| 458 | case !tests[r.Witness]: |
| 459 | t.Errorf("%s %s %s: the witness %s is not a test in this repository", r.File, r.Func, r.Callee, r.Witness) |
| 460 | } |
| 461 | } |
| 462 | }) |
| 463 | |
| 464 | // The fixtures: each edit the inventory exists to catch. |
| 465 | t.Run("a new Commit site supplying a roster region is unknown", func(t *testing.T) { |
| 466 | src := "package host\n\nimport \"github.com/carlosframework/platform/internal/hostclaim\"\n\n" + |
| 467 | "func homeRegionFixture(rr regionRow) error {\n\treturn hostclaim.Commit(id, from, src, rr.row.Region, scope, staging, newID)(c)\n}\n" |
| 468 | found, err := scanHomeRegionSites(".", map[string]string{"internal/host/zz_home_region_fixture.go": src}) |
| 469 | if err != nil { |
| 470 | t.Fatal(err) |
| 471 | } |
| 472 | homeRegionWant(t, homeRegionDiff(found, homeRegionCallSites), "unknown site", "internal/host/zz_home_region_fixture.go", "hostclaim.Commit(targetRegion)", "rr.row.Region") |
| 473 | }) |
| 474 | t.Run("home_region handed to QueueStagingPark at a new site is unknown", func(t *testing.T) { |
| 475 | src := "package host\n\nimport (\n\t\"github.com/carlosframework/platform/internal/hostclaim\"\n\t\"github.com/carlosframework/platform/internal/park\"\n)\n\n" + |
| 476 | "func homeRegionFixture(c *hostclaim.Claim) error {\n\tto := c.Op.Intent[\"home_region\"]\n\treturn hostclaim.QueueStagingPark(id, park.PlacementRegion(to), scope, newID)(c)\n}\n" |
| 477 | found, err := scanHomeRegionSites(".", map[string]string{"internal/host/zz_home_region_fixture.go": src}) |
| 478 | if err != nil { |
| 479 | t.Fatal(err) |
| 480 | } |
| 481 | homeRegionWant(t, homeRegionDiff(found, homeRegionCallSites), "unknown site", "hostclaim.QueueStagingPark(region)", "park.PlacementRegion(to)") |
| 482 | }) |
| 483 | t.Run("home_region handed to QueueStagingPark at an inventoried site is changed", func(t *testing.T) { |
| 484 | overlay := homeRegionOverlay(t, "internal/host/movesrollback.go", |
| 485 | "scope := moves.StagingScope(host, id, obs.Claim.Holder.Incarnation, staged.Bound())", |
| 486 | "scope := moves.StagingScope(host, id, obs.Claim.Holder.Incarnation, staged.Bound())\n\t\tto := obs.Claim.Op.Intent[\"home_region\"]", |
| 487 | "hostclaim.QueueStagingPark(id, cfg.parkRegion(), scope, obligationID)", |
| 488 | "hostclaim.QueueStagingPark(id, park.PlacementRegion(to), scope, obligationID)") |
| 489 | found, err := scanHomeRegionSites(".", overlay) |
| 490 | if err != nil { |
| 491 | t.Fatal(err) |
| 492 | } |
| 493 | homeRegionWant(t, homeRegionDiff(found, homeRegionCallSites), "changed site", "rollbackRun.remote", "hostclaim.QueueStagingPark(region)", "park.PlacementRegion(to)") |
| 494 | }) |
| 495 | t.Run("a FencedTargetRelease naming home_region is changed", func(t *testing.T) { |
| 496 | found, err := scanHomeRegionSites(".", homeRegionOverlay(t, "internal/hostops/move_fence_release.go", "TargetRegion: park.PlacementRegion(m.To)", |
| 497 | `TargetRegion: o.Claim.Op.Intent["home_region"]`)) |
| 498 | if err != nil { |
| 499 | t.Fatal(err) |
| 500 | } |
| 501 | homeRegionWant(t, homeRegionDiff(found, homeRegionCallSites), "changed site", "hostclaim.FencedTargetRelease{TargetRegion}", `o.Claim.Op.Intent["home_region"]`) |
| 502 | }) |
| 503 | t.Run("a site that no longer exists leaves a stale row", func(t *testing.T) { |
| 504 | found, err := scanHomeRegionSites(".", homeRegionOverlay(t, "internal/host/movespass.go", "return moves.StagingLeasePrefix(r, host), nil", |
| 505 | `return "control/staging-leases/" + r + "/" + host, nil`)) |
| 506 | if err != nil { |
| 507 | t.Fatal(err) |
| 508 | } |
| 509 | homeRegionWant(t, homeRegionDiff(found, homeRegionCallSites), "stale row", "stagingLeaseKey", "moves.StagingLeasePrefix(targetRegion)") |
| 510 | }) |
| 511 | t.Run("an aliased import is still found", func(t *testing.T) { |
| 512 | src := "package host\n\nimport hc \"github.com/carlosframework/platform/internal/hostclaim\"\n\n" + |
| 513 | "func homeRegionFixture() error {\n\treturn hc.Touch(id, []string{\"ie\"}, nil)(c)\n}\n" |
| 514 | found, err := scanHomeRegionSites(".", map[string]string{"internal/console/zz_home_region_fixture.go": src}) |
| 515 | if err != nil { |
| 516 | t.Fatal(err) |
| 517 | } |
| 518 | homeRegionWant(t, homeRegionDiff(found, homeRegionCallSites), "unknown site", "hostclaim.Touch(regions)", `[]string{"ie"}`) |
| 519 | }) |
| 520 | } |
| 521 | |