carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostfiles
2
3import (
4 "context"
5 "path/filepath"
6 "testing"
7
8 "github.com/carlosframework/platform/internal/registry"
9 "github.com/carlosframework/platform/internal/release"
10)
11
12func wakeReg(t *testing.T) *registry.Registry {
13 t.Helper()
14 r, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
15 if err != nil {
16 t.Fatal(err)
17 }
18 t.Cleanup(func() { r.Close() })
19 return r
20}
21
22func seedHost(t *testing.T, r *registry.Registry, host string) {
23 t.Helper()
24 if err := r.Upsert(registry.Route{Host: host, Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Hibernate: true}); err != nil {
25 t.Fatal(err)
26 }
27}
28
29func TestWakeStateRoundTripsThroughTheManifest(t *testing.T) {
30 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
31 src, dst := wakeReg(t), wakeReg(t)
32 seedHost(t, src, "shop.test")
33 seedHost(t, dst, "shop.test")
34 if err := src.ReplaceTimers("shop.test", []registry.Timer{
35 {Host: "shop.test", Name: "nightly", Kind: "schedule", Spec: "0 3 * * *", Path: "/n", NextAt: 5000, LastAt: 4000,
36 LastStatus: "ok", Attempts: 1, CreatedAt: 10, DueAt: 4900, Registration: "reg-n"},
37 {Host: "shop.test", Name: "remind", Kind: "once", Spec: "2026-10-06T00:00:00Z", Path: "/r", NextAt: 6000, CreatedAt: 11, DueAt: 6000, Registration: "reg-r"},
38 {Host: "shop.test", Name: "done", Kind: "once", Spec: "2026-10-01T00:00:00Z", Path: "/d", NextAt: 0, LastAt: 3000, CreatedAt: 9, Registration: "reg-d"},
39 }); err != nil {
40 t.Fatal(err)
41 }
42 if err := src.SetNextDue("shop.test", 7777); err != nil {
43 t.Fatal(err)
44 }
45 if err := src.SetReplication("shop.test", registry.ReplicationBatched); err != nil {
46 t.Fatal(err)
47 }
48
49 ws, err := ExportWake(src, "shop.test")
50 if err != nil {
51 t.Fatal(err)
52 }
53 if len(ws.Timers) != 2 {
54 t.Fatalf("exported %d timers, want 2: a finished once-timer is history, not wake state", len(ws.Timers))
55 }
56 p := open(t, s)
57 if err := StageWake(ctx, p, ws); err != nil {
58 t.Fatal(err)
59 }
60 commit(t, p)
61
62 m, _, err := Load(ctx, s, pfx)
63 if err != nil {
64 t.Fatal(err)
65 }
66 got, found, err := LoadWake(ctx, s, pfx, m, nil)
67 if err != nil || !found {
68 t.Fatalf("LoadWake found=%v err=%v", found, err)
69 }
70 if err := ImportWake(dst, "shop.test", got); err != nil {
71 t.Fatal(err)
72 }
73 rows, _ := dst.TimersFor("shop.test")
74 if len(rows) != 2 || rows[0].Name != "nightly" || rows[0].Registration != "reg-n" || rows[0].NextAt != 5000 || rows[0].Attempts != 1 {
75 t.Fatalf("imported timers: %+v", rows)
76 }
77 rt, err := dst.Get("shop.test")
78 if err != nil || rt == nil {
79 t.Fatalf("route: %v %v", rt, err)
80 }
81 if rt.NextDue != 7777 || rt.Replication != registry.ReplicationBatched {
82 t.Fatalf("NextDue=%d Replication=%q", rt.NextDue, rt.Replication)
83 }
84}
85
86func TestStageWakeOfUnchangedStateStagesNothing(t *testing.T) {
87 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
88 p := open(t, s)
89 ws := WakeState{V: 1, Host: "shop.test", NextDue: 5}
90 if err := StageWake(ctx, p, ws); err != nil {
91 t.Fatal(err)
92 }
93 commit(t, p)
94 if err := StageWake(ctx, p, ws); err != nil {
95 t.Fatal(err)
96 }
97 if p.Dirty() {
98 t.Fatal("unchanged wake state was staged again")
99 }
100}
101
102func TestLoadWakeWithNoneReportsNotFound(t *testing.T) {
103 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
104 m, _, _ := Load(ctx, s, pfx)
105 if _, found, err := LoadWake(ctx, s, pfx, m, nil); found || err != nil {
106 t.Fatalf("found=%v err=%v", found, err)
107 }
108}
109
110// A manifest that names wake state whose object has gone is lost data,
111// not a host without timers.
112func TestLoadWakeWithItsObjectMissingIsAnError(t *testing.T) {
113 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
114 p := open(t, s)
115 if err := StageWake(ctx, p, WakeState{V: 1, Host: "shop.test", NextDue: 5}); err != nil {
116 t.Fatal(err)
117 }
118 commit(t, p)
119 m, _, err := Load(ctx, s, pfx)
120 if err != nil {
121 t.Fatal(err)
122 }
123 if err := s.Delete(ctx, m.Platform[WakeItem].Object); err != nil {
124 t.Fatal(err)
125 }
126 if _, found, err := LoadWake(ctx, s, pfx, m, nil); err == nil || found {
127 t.Fatalf("found = %v, err = %v; a missing wake object must be an error", found, err)
128 }
129}
130
131func TestImportWakeRefusesAnotherHostsState(t *testing.T) {
132 r := wakeReg(t)
133 seedHost(t, r, "shop.test")
134 if err := ImportWake(r, "shop.test", WakeState{V: 1, Host: "other.test"}); err == nil {
135 t.Fatal("imported another host's wake state")
136 }
137}
138
139func TestImportWakeRefusesAnUnknownReplicationClassAndChangesNothing(t *testing.T) {
140 r := wakeReg(t)
141 seedHost(t, r, "shop.test")
142 if err := r.ReplaceTimers("shop.test", []registry.Timer{{Host: "shop.test", Name: "keep", Kind: "schedule", Spec: "@every 1m", Path: "/k", NextAt: 1, CreatedAt: 1}}); err != nil {
143 t.Fatal(err)
144 }
145 err := ImportWake(r, "shop.test", WakeState{V: 1, Host: "shop.test", Replication: "hourly"})
146 if err == nil {
147 t.Fatal("accepted replication class \"hourly\"")
148 }
149 if rows, _ := r.TimersFor("shop.test"); len(rows) != 1 {
150 t.Fatalf("a refused import changed the timers: %+v", rows)
151 }
152}
153
154func TestExportWakeOfAnUnknownHostIsAnError(t *testing.T) {
155 if _, err := ExportWake(wakeReg(t), "nobody.test"); err == nil {
156 t.Fatal("exported wake state for a host with no route")
157 }
158}
159
160func TestImportWakeOfTheDefaultClassReplacesAnotherClass(t *testing.T) {
161 r := wakeReg(t)
162 seedHost(t, r, "shop.test")
163 if err := r.SetReplication("shop.test", registry.ReplicationDaily); err != nil {
164 t.Fatal(err)
165 }
166 if err := ImportWake(r, "shop.test", WakeState{V: 1, Host: "shop.test", Replication: ""}); err != nil {
167 t.Fatal(err)
168 }
169 rt, err := r.Get("shop.test")
170 if err != nil || rt == nil {
171 t.Fatalf("route: %v %v", rt, err)
172 }
173 if rt.Replication != "" {
174 t.Fatalf("Replication = %q, want the default", rt.Replication)
175 }
176}
177
178func TestImportWakeForAHostWithNoRouteWritesNothing(t *testing.T) {
179 r := wakeReg(t)
180 if err := r.ReplaceTimers("ghost.test", []registry.Timer{{Host: "ghost.test", Name: "keep", Kind: "schedule", Spec: "@every 1m", Path: "/k", NextAt: 1, CreatedAt: 1}}); err != nil {
181 t.Fatal(err)
182 }
183 ws := WakeState{V: 1, Host: "ghost.test", Timers: []WakeTimer{{Name: "new", Kind: "schedule", Spec: "@every 2m", Path: "/n", NextAt: 2, CreatedAt: 2}}}
184 if err := ImportWake(r, "ghost.test", ws); err == nil {
185 t.Fatal("imported wake state for a host with no route")
186 }
187 rows, _ := r.TimersFor("ghost.test")
188 if len(rows) != 1 || rows[0].Name != "keep" {
189 t.Fatalf("a refused import changed the timers: %+v", rows)
190 }
191}
192