R-356: the off-site restore refused every app that has no data drive
gates / gates (push) Failing after 12s
gates / gates (push) Failing after 12s
ReconstituteFromOffsite and PlaceOffsiteRestore both resolved the restore destination with the RAW HDD_PATH and read an empty answer as "the app is not installed". For 40 of the 53 catalogue apps that answer is correctly empty and permanent, so both actions refused forever for a running, healthy app — and told the customer to reinstall it "in the same place", which those apps never offer. Separate the two questions. "Installed?" is asked of ListDeployedStacks via a new Manager.isStackDeployed that fails CLOSED on a nil provider. "Where?" is answered by GetAppDrivePath — the same resolver CaptureRecoveryUnit wrote the snapshot with, so the restore aims at the place the backup came from. The 13 drive apps are unchanged: own drive, mismatch check, ack still required. A third refusal, with its own sentence, covers installed-but-no-resolvable-root. Fixtures that marked an app "installed" by giving it an HDD path now state deployment as its own fact. No assertion weakened.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// R-356 §10 seam discipline. The restore now decides "is this app installed?" from
|
||||
// ListDeployedStacks. That answer only means anything if the PRODUCTION adapter is the thing
|
||||
// answering it, and if that adapter really filters on Deployed — a component that is correct in a
|
||||
// fake and unreachable in production is the four-times-shipped defect class in this repo.
|
||||
//
|
||||
// AST, never strings.Contains: a commented-out call still satisfies a substring match.
|
||||
|
||||
// funcBody returns the named top-level func's body from main.go, or fails.
|
||||
func funcBodyInMain(t *testing.T, name string) *ast.BlockStmt {
|
||||
t.Helper()
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "main.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse main.go: %v", err)
|
||||
}
|
||||
for _, decl := range f.Decls {
|
||||
fn, ok := decl.(*ast.FuncDecl)
|
||||
if !ok || fn.Body == nil || fn.Name.Name != name {
|
||||
continue
|
||||
}
|
||||
return fn.Body
|
||||
}
|
||||
t.Fatalf("func %s not found in main.go", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestMainWiresTheStackProviderIntoTheBackupManager: without this call the backup manager's
|
||||
// stackProvider is nil, isStackDeployed fails closed, and EVERY off-site restore refuses.
|
||||
func TestMainWiresTheStackProviderIntoTheBackupManager(t *testing.T) {
|
||||
body := funcBodyInMain(t, "main")
|
||||
var found bool
|
||||
ast.Inspect(body, func(n ast.Node) bool {
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok || sel.Sel.Name != "SetStackProvider" || len(call.Args) != 1 {
|
||||
return true
|
||||
}
|
||||
found = true
|
||||
return false
|
||||
})
|
||||
if !found {
|
||||
t.Fatal("main() never calls SetStackProvider — the restore's deployment check would fail closed for every app")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStackAdapterListDeployedFiltersOnDeployed pins the semantics R-356 depends on: the adapter
|
||||
// returns DEPLOYED stacks, not all known ones. Verified by the presence of the `!s.Deployed`
|
||||
// continue-guard in the method body — remove it and an app that was never deployed would be told it
|
||||
// has somewhere to restore to.
|
||||
func TestStackAdapterListDeployedFiltersOnDeployed(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "main.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse main.go: %v", err)
|
||||
}
|
||||
var body *ast.BlockStmt
|
||||
for _, decl := range f.Decls {
|
||||
fn, ok := decl.(*ast.FuncDecl)
|
||||
if !ok || fn.Recv == nil || fn.Name.Name != "ListDeployedStacks" || fn.Body == nil {
|
||||
continue
|
||||
}
|
||||
body = fn.Body
|
||||
}
|
||||
if body == nil {
|
||||
t.Fatal("stackAdapter.ListDeployedStacks not found in main.go")
|
||||
}
|
||||
var guarded bool
|
||||
ast.Inspect(body, func(n ast.Node) bool {
|
||||
un, ok := n.(*ast.UnaryExpr)
|
||||
if !ok || un.Op != token.NOT {
|
||||
return true
|
||||
}
|
||||
sel, ok := un.X.(*ast.SelectorExpr)
|
||||
if ok && sel.Sel.Name == "Deployed" {
|
||||
guarded = true
|
||||
}
|
||||
return true
|
||||
})
|
||||
if !guarded {
|
||||
t.Fatal("ListDeployedStacks does not test !s.Deployed — an undeployed app would be reported installed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user