package host import ( "encoding/json" "os" "path/filepath" "sort" "strings" "testing" "github.com/carlosframework/platform/internal/hostclaim" ) // retirementsByOp is how F3 finds a host's local retirements by op, one rule // for both receipts a retirement can carry (K20): park.Local's .journal.json // (Op) and the moves-owned rename's moves-retirement.json (op), each in the // event directory .parked////. It returns the // retirement ids whose receipt names op, and those with no readable receipt. // This is the one copy of the rule; internal/park's test checks only that // park.Local's journal carries Op. func retirementsByOp(t *testing.T, dataDir, incarnation, host, op string) (found, unreadable []string) { t.Helper() dir := filepath.Join(dataDir, ".parked", incarnation, host) es, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } for _, e := range es { var engine struct{ Op string } var owned struct { Op string `json:"op"` } named := "" if b, err := os.ReadFile(filepath.Join(dir, e.Name(), ".journal.json")); err == nil && json.Unmarshal(b, &engine) == nil { named = engine.Op } else if b, err := os.ReadFile(filepath.Join(dir, e.Name(), localParkReceiptName)); err == nil && json.Unmarshal(b, &owned) == nil { named = owned.Op } else { unreadable = append(unreadable, e.Name()) continue } if named == op { found = append(found, e.Name()) } } sort.Strings(found) return found, unreadable } // TestMoveLocalRetirementsAreFindableByOp (moves spec ยง6.4, D6, K20): a // move leaves two local receipts, not one. A registered source's // finalization retires its installation through park.Local (.journal.json) // and parks what the inventory does not list by the moves-owned rename // (moves-retirement.json), each in its own retirement under the engine's // layout. Both name the move's op, and nothing under the host's // incarnation lacks a receipt, so F3 finds every local retirement of the // move by op with one rule. func TestMoveLocalRetirementsAreFindableByOp(t *testing.T) { t.Parallel() h := newFinalHarness(t) dd := h.cfg.DataDir for _, d := range []string{"hostguard", ".parked"} { if err := os.MkdirAll(filepath.Join(dd, d), 0o770); err != nil { t.Fatal(err) } } rec := `{"v":1,"host":"` + p1Host + `","incarnation":"` + h.inc + `","active_stamp":1,"retired_stamps":[]}` if err := os.WriteFile(filepath.Join(dd, "hostguard", p1Host+".json"), []byte(rec), 0o660); err != nil { t.Fatal(err) } rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { if c.Inventory == nil { c.Inventory = map[string]hostclaim.InventoryEntry{} } c.Inventory["ie-box"] = hostclaim.InventoryEntry{Layout: "flat", Paths: map[string]uint64{p1Host + ".db": 1, p1Host + ".db.gen": 1, p1Host: 1, p1Host + ".env": 1}} }) h.commit(t) var chore hostclaim.Obligation for _, o := range h.claim(t).Obligations { if o.Kind == "park-local" { chore = o } } if chore.Op != p1ID { t.Fatalf("the commit queued no park-local of the move: %+v", h.claim(t).Obligations) } h.sourcePass(t) owned := h.parkedJournal(t).Steps[finalStepLocal] if owned == "" || owned == chore.Retirement { t.Fatalf("the moves-owned retirement %q, park.Local's %q", owned, chore.Retirement) } for _, receipt := range []string{filepath.Join(chore.Retirement, ".journal.json"), filepath.Join(owned, localParkReceiptName)} { if _, err := os.Stat(filepath.Join(dd, ".parked", h.inc, p1Host, receipt)); err != nil { t.Fatalf("the receipt %s: %v", receipt, err) } } found, unreadable := retirementsByOp(t, dd, h.inc, p1Host, p1ID) want := []string{chore.Retirement, owned} sort.Strings(want) if strings.Join(found, " ") != strings.Join(want, " ") || len(unreadable) != 0 { t.Fatalf("by op %s: found %v (want %v), no receipt %v", p1ID, found, want, unreadable) } if other, _ := retirementsByOp(t, dd, h.inc, p1Host, "mv_zzzzzzzzzzzzzzzzzzzzzzzzzz"); len(other) != 0 { t.Fatalf("another op found the move's retirements: %v", other) } }