carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/provision"
11)
12
13// pagesSeen records the pages a box arm sends through moveCfg.Page.
14type pagesSeen []struct{ level, subject, body string }
15
16func (p *pagesSeen) hook() func(context.Context, string, string, string) error {
17 return func(_ context.Context, level, subject, body string) error {
18 *p = append(*p, struct{ level, subject, body string }{level, subject, body})
19 return nil
20 }
21}
22
23// stuckPageBody is the page the record's stuck mark sends (MV-N314), spelled
24// out here rather than built by the code under test.
25func stuckPageBody(box, role, reason string) string {
26 return "The move " + p1ID + " of " + p1Host + " stopped for an operator on " + box + ", its " + role + ": " + reason +
27 ". Run carlos move resolve --host " + p1Host + " --retry once the cause is fixed."
28}
29
30// pagedOnce asserts exactly one incident page for the stuck mark, carrying
31// no path and no secret.
32func pagedOnce(t *testing.T, pages pagesSeen, box, role, reason string) {
33 t.Helper()
34 if len(pages) != 1 {
35 t.Fatalf("pages %+v, want one", pages)
36 }
37 p := pages[0]
38 if p.level != "incident" || p.subject != "carlos move stuck: "+p1Host || p.body != stuckPageBody(box, role, reason) {
39 t.Fatalf("page %+v\nwant body %q", p, stuckPageBody(box, role, reason))
40 }
41 if strings.Contains(p.subject+p.body, "/") {
42 t.Fatalf("a page carries a path: %+v", p)
43 }
44}
45
46// TestStuckMarksPageAnOperator (moves spec §13, K11): wherever a box arm's
47// MoveMarkStuck lands, the operator is paged once, as an incident, with the
48// recorded reason; a pass over the stuck record pages nothing more. The
49// hook is nil in the other tests and in production until armMovesPass sets
50// it to PageOperator.
51func TestStuckMarksPageAnOperator(t *testing.T) {
52 t.Parallel()
53 t.Run("the target's return", func(t *testing.T) {
54 h := newRollbackHarness(t)
55 db := provision.DefaultDBPath(h.tdir, p1Host)
56 if err := os.Rename(db, db+".aside"); err != nil {
57 t.Fatal(err)
58 }
59 h.rollBack(t)
60 h.rollbackPass(t)
61 h.stuckAtReturning(t, provision.StuckReturnTargetDatabase)
62 h.rollbackPass(t)
63 pagedOnce(t, h.pages, "uk-box", "target", provision.StuckReturnTargetDatabase)
64 })
65 t.Run("the source's recovery", func(t *testing.T) {
66 h := newRecoverHarness(t, false)
67 h.released(t)
68 if err := os.WriteFile(filepath.Join(h.sdir, p1Host, "upload.txt"), []byte("changed on the source"), 0o644); err != nil {
69 t.Fatal(err)
70 }
71 h.pages = nil
72 h.srcPass(t)
73 if m := h.read(t, p1Host).Move; m.State != provision.MoveStuck || m.StuckReason != provision.StuckBaselineMismatch {
74 t.Fatalf("not stuck: %+v", m)
75 }
76 h.srcPass(t)
77 pagedOnce(t, h.pages, "ie-box", "source", provision.StuckBaselineMismatch)
78 })
79 t.Run("the source's dirty return install", func(t *testing.T) {
80 h := newRecoverHarness(t, true)
81 h.released(t)
82 sentinel := filepath.Join(h.sdir, p1Host, "sentinel.txt")
83 h.landFault = func(p string) error {
84 if p == "after-"+landStepRetired {
85 if err := os.MkdirAll(filepath.Dir(sentinel), 0o755); err != nil {
86 t.Fatal(err)
87 }
88 return os.WriteFile(sentinel, []byte("not the return's"), 0o644)
89 }
90 return nil
91 }
92 h.pages = nil
93 h.srcPass(t)
94 if m := h.read(t, p1Host).Move; m.State != provision.MoveStuck || m.StuckReason != provision.StuckReturnFilesOccupied {
95 t.Fatalf("not stuck: %+v", m)
96 }
97 h.srcPass(t)
98 pagedOnce(t, h.pages, "ie-box", "source", provision.StuckReturnFilesOccupied)
99 })
100 t.Run("the source's revert", func(t *testing.T) {
101 h := newRevertHarness(t)
102 h.fromStopped(t)
103 h.arm(t)
104 var pages pagesSeen
105 h.cfg.Page = pages.hook()
106 if err := os.WriteFile(filepath.Join(h.cfg.DataDir, p1Host, "upload.txt"), []byte("changed while stopped"), 0o644); err != nil {
107 t.Fatal(err)
108 }
109 h.pass(t)
110 if m := h.move(t); m.State != provision.MoveStuck || m.StuckReason != provision.StuckBaselineMismatch {
111 t.Fatalf("not stuck: %+v", m)
112 }
113 h.pass(t)
114 pagedOnce(t, pages, "ie-box", "source", provision.StuckBaselineMismatch)
115 })
116}
117