package console import ( "go/ast" "go/parser" "go/token" "io/fs" "path/filepath" "strings" "testing" ) func moveRecoveryPolicyBound(f *ast.File) bool { bound := false named := func(e ast.Expr) bool { switch e := e.(type) { case *ast.Ident: return e.Name == "ClaimMoveRecovery" case *ast.SelectorExpr: return e.Sel.Name == "ClaimMoveRecovery" } return false } nonnil := func(e ast.Expr) bool { id, ok := e.(*ast.Ident); return !ok || id.Name != "nil" } ast.Inspect(f, func(n ast.Node) bool { switch n := n.(type) { case *ast.KeyValueExpr: if named(n.Key) && nonnil(n.Value) { bound = true } case *ast.AssignStmt: for i, lhs := range n.Lhs { if named(lhs) && (i >= len(n.Rhs) || nonnil(n.Rhs[i])) { bound = true } } } return true }) return bound } func releaseBeforeClose(f *ast.File, fn, release, close string) bool { for _, d := range f.Decls { f, ok := d.(*ast.FuncDecl) if !ok || f.Name.Name != fn { continue } var released, closed token.Pos ast.Inspect(f.Body, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return true } if sel.Sel.Name == release && released == 0 { released = call.Pos() } if sel.Sel.Name == close && closed == 0 { closed = call.Pos() } return true }) return released != 0 && (closed == 0 || released < closed) } return false } func TestClaimMoveRecoveryWiringWaitsForRecordFirstOwners(t *testing.T) { root := "../.." var owners []string for _, owner := range []struct{ file, fn, release, close string }{ {"movesparks.go", "closeMove", "ReleaseMove", "MoveClose"}, {"movesrecover.go", "finalize", "ReleaseMove", "MoveRolledBackClose"}, {"movesrevert.go", "opened", "ReleaseMove", "close"}, {"movesabort.go", "abortSource", "releaseAbort", "MoveGuardedAbortComplete"}, } { path := filepath.Join(root, "internal/host", owner.file) f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) if err != nil { t.Fatal(err) } if releaseBeforeClose(f, owner.fn, owner.release, owner.close) { owners = append(owners, owner.file+":"+owner.fn) } } if len(owners) == 0 { return } err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { if path != root && (strings.HasPrefix(d.Name(), ".") || d.Name() == "testdata" || d.Name() == "vendor") { return filepath.SkipDir } return nil } if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { return nil } f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) if err != nil { return err } if moveRecoveryPolicyBound(f) { t.Errorf("%s wires ClaimMoveRecovery while owners release first: %v", path, owners) } return nil }) if err != nil { t.Fatal(err) } } func TestClaimMoveRecoveryWiringGuardDetectsBindings(t *testing.T) { for _, src := range []string{ `package p; func f(){ c.ClaimMoveRecovery = policy }`, `package p; var c = Config{ClaimMoveRecovery: policy}`, } { f, err := parser.ParseFile(token.NewFileSet(), "fixture.go", src, 0) if err != nil { t.Fatal(err) } if !moveRecoveryPolicyBound(f) { t.Fatal("binding escaped census") } } for _, src := range []string{ `package p; func f(){ c.ClaimMoveRecovery = nil }`, `package p; var c = Config{ClaimMoveRecovery: nil}`, } { f, err := parser.ParseFile(token.NewFileSet(), "fixture.go", src, 0) if err != nil { t.Fatal(err) } if moveRecoveryPolicyBound(f) { t.Fatal("nil policy counted as binding") } } for _, tc := range []struct { src string want bool }{ {`package p; func owner(){hostclaim.ReleaseMove();provision.MoveClose()}`, true}, {`package p; func owner(){provision.MoveClose();hostclaim.ReleaseMove()}`, false}, } { f, err := parser.ParseFile(token.NewFileSet(), "fixture.go", tc.src, 0) if err != nil { t.Fatal(err) } if got := releaseBeforeClose(f, "owner", "ReleaseMove", "MoveClose"); got != tc.want { t.Fatalf("release ordering=%v", got) } } }