carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7 "time"
8)
9
10// stage writes a version tree the way StageArtifact lays one out, and dates
11// every file it wrote `age` in the past.
12func stage(t *testing.T, optDir, acct, ver, app string, size int, age time.Duration) string {
13 t.Helper()
14 p := VersionPath(optDir, acct, ver, app)
15 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
16 t.Fatal(err)
17 }
18 if err := os.WriteFile(p, make([]byte, size), 0o755); err != nil {
19 t.Fatal(err)
20 }
21 when := time.Now().Add(-age)
22 if err := os.Chtimes(p, when, when); err != nil {
23 t.Fatal(err)
24 }
25 return p
26}
27
28func TestSweepStagedRemovesOnlyUnpinnedAndOld(t *testing.T) {
29 opt := t.TempDir()
30 stage(t, opt, OpsAccount, "live", "edge", 10, 48*time.Hour)
31 stage(t, opt, OpsAccount, "pinned-by-registry", "edge", 20, 48*time.Hour)
32 stage(t, opt, "acct1", "old", "blog", 30, 48*time.Hour)
33 stage(t, opt, "acct1", "fresh", "blog", 40, time.Minute)
34
35 if err := PointLive(opt, "edge.example.com", OpsAccount, "live", "edge"); err != nil {
36 t.Fatal(err)
37 }
38 pins, err := LivePins(opt)
39 if err != nil {
40 t.Fatal(err)
41 }
42 if !pins["live"] || len(pins) != 1 {
43 t.Fatalf("LivePins = %v, want just {live}", pins)
44 }
45 pins["pinned-by-registry"] = true
46
47 res, err := SweepStaged(opt, pins, 6*time.Hour, time.Now())
48 if err != nil {
49 t.Fatal(err)
50 }
51 if len(res.Removed) != 1 || res.Removed[0] != "old" {
52 t.Fatalf("Removed = %v, want [old]", res.Removed)
53 }
54 if res.Bytes != 30 {
55 t.Errorf("Bytes = %d, want 30", res.Bytes)
56 }
57 if res.Pinned != 2 || res.Young != 1 || res.Scanned != 4 {
58 t.Errorf("res = %+v, want 2 pinned, 1 young, 4 scanned", res)
59 }
60 if len(res.Errs) != 0 {
61 t.Errorf("Errs = %v, want none", res.Errs)
62 }
63 for _, keep := range []string{"live", "pinned-by-registry", "fresh"} {
64 if _, err := os.Stat(filepath.Join(opt, "versions", keep)); err != nil {
65 t.Errorf("%s was removed: %v", keep, err)
66 }
67 }
68 if _, err := os.Stat(filepath.Join(opt, "versions", "old")); !os.IsNotExist(err) {
69 t.Errorf("old survived: %v", err)
70 }
71 // The live link must still resolve — the point of the pin.
72 if _, err := os.Stat(LiveLink(opt, "edge.example.com")); err != nil {
73 t.Errorf("live link broken: %v", err)
74 }
75}
76
77// A version dir holds every account's build at that version, so a pin from
78// one account keeps the whole directory — including a second account's app
79// inside it.
80func TestSweepStagedPinsWholeVersionDir(t *testing.T) {
81 opt := t.TempDir()
82 stage(t, opt, "acct1", "v1", "blog", 10, 48*time.Hour)
83 stage(t, opt, "acct2", "v1", "shop", 10, 48*time.Hour)
84
85 res, err := SweepStaged(opt, map[string]bool{"v1": true}, 6*time.Hour, time.Now())
86 if err != nil {
87 t.Fatal(err)
88 }
89 if len(res.Removed) != 0 {
90 t.Fatalf("Removed = %v, want none", res.Removed)
91 }
92 if _, err := os.Stat(VersionPath(opt, "acct2", "v1", "shop")); err != nil {
93 t.Errorf("acct2's build removed by acct1's pin: %v", err)
94 }
95}
96
97// The age of a tree is its newest FILE, not its newest directory: deleting
98// entries bumps the parent's mtime, and a half-swept tree that read as
99// freshly written would renew its own grace forever.
100func TestSweepStagedAgesOnFileMtimeNotDirMtime(t *testing.T) {
101 opt := t.TempDir()
102 p := stage(t, opt, "acct1", "v1", "blog", 10, 48*time.Hour)
103 // A sibling left behind and then removed, as a partial RemoveAll would:
104 // the parent directory's mtime is now.
105 sib := filepath.Join(filepath.Dir(p), "leftover")
106 if err := os.WriteFile(sib, []byte("x"), 0o644); err != nil {
107 t.Fatal(err)
108 }
109 if err := os.Remove(sib); err != nil {
110 t.Fatal(err)
111 }
112
113 res, err := SweepStaged(opt, nil, 6*time.Hour, time.Now())
114 if err != nil {
115 t.Fatal(err)
116 }
117 if len(res.Removed) != 1 {
118 t.Fatalf("Removed = %v, want [v1] — a fresh dir mtime must not renew the grace", res.Removed)
119 }
120}
121
122// An emptied husk has no file to age it and must still be collectable.
123func TestSweepStagedRemovesEmptyHusk(t *testing.T) {
124 opt := t.TempDir()
125 dir := filepath.Join(opt, "versions", "v1")
126 if err := os.MkdirAll(dir, 0o755); err != nil {
127 t.Fatal(err)
128 }
129 when := time.Now().Add(-48 * time.Hour)
130 if err := os.Chtimes(dir, when, when); err != nil {
131 t.Fatal(err)
132 }
133 res, err := SweepStaged(opt, nil, 6*time.Hour, time.Now())
134 if err != nil {
135 t.Fatal(err)
136 }
137 if len(res.Removed) != 1 {
138 t.Fatalf("Removed = %v, want [v1]", res.Removed)
139 }
140}
141
142// Entries this package did not write are not ours to delete, however old.
143func TestSweepStagedLeavesForeignEntries(t *testing.T) {
144 opt := t.TempDir()
145 vdir := filepath.Join(opt, "versions")
146 if err := os.MkdirAll(vdir, 0o755); err != nil {
147 t.Fatal(err)
148 }
149 loose := filepath.Join(vdir, "notes.txt")
150 if err := os.WriteFile(loose, []byte("hand-written"), 0o644); err != nil {
151 t.Fatal(err)
152 }
153 odd := filepath.Join(vdir, "backup 2026") // a space: ValidVersion rejects it
154 if err := os.MkdirAll(odd, 0o755); err != nil {
155 t.Fatal(err)
156 }
157 when := time.Now().Add(-30 * 24 * time.Hour)
158 for _, p := range []string{loose, odd} {
159 if err := os.Chtimes(p, when, when); err != nil {
160 t.Fatal(err)
161 }
162 }
163
164 res, err := SweepStaged(opt, nil, 6*time.Hour, time.Now())
165 if err != nil {
166 t.Fatal(err)
167 }
168 if len(res.Removed) != 0 {
169 t.Fatalf("Removed = %v, want none", res.Removed)
170 }
171 if res.Skipped != 2 {
172 t.Errorf("Skipped = %d, want 2", res.Skipped)
173 }
174 for _, p := range []string{loose, odd} {
175 if _, err := os.Stat(p); err != nil {
176 t.Errorf("%s removed: %v", p, err)
177 }
178 }
179}
180
181func TestSweepStagedNoVersionsDir(t *testing.T) {
182 res, err := SweepStaged(t.TempDir(), nil, 6*time.Hour, time.Now())
183 if err != nil {
184 t.Fatalf("a box that has never staged anything is not an error: %v", err)
185 }
186 if res.Scanned != 0 {
187 t.Errorf("Scanned = %d, want 0", res.Scanned)
188 }
189}
190
191// A `<host>.tmp` link from a crashed PointLive names a real staged tree and
192// pins it: reading every entry is cheaper than deciding which links are
193// current.
194func TestLivePinsIncludesTempLinks(t *testing.T) {
195 opt := t.TempDir()
196 stage(t, opt, OpsAccount, "v1", "edge", 10, time.Hour)
197 stage(t, opt, OpsAccount, "v2", "edge", 10, time.Hour)
198 if err := PointLive(opt, "edge.example.com", OpsAccount, "v1", "edge"); err != nil {
199 t.Fatal(err)
200 }
201 if err := os.Symlink(VersionPath(opt, OpsAccount, "v2", "edge"), LiveLink(opt, "edge.example.com")+".tmp"); err != nil {
202 t.Fatal(err)
203 }
204 pins, err := LivePins(opt)
205 if err != nil {
206 t.Fatal(err)
207 }
208 if !pins["v1"] || !pins["v2"] {
209 t.Fatalf("LivePins = %v, want v1 and v2", pins)
210 }
211}
212
213// A dangling link still pins: the version it names is what the unit's
214// ExecStart resolves through, and the sweep must not widen the hole.
215func TestLivePinsDanglingLink(t *testing.T) {
216 opt := t.TempDir()
217 if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
218 t.Fatal(err)
219 }
220 if err := os.Symlink(VersionPath(opt, OpsAccount, "gone", "edge"), LiveLink(opt, "edge.example.com")); err != nil {
221 t.Fatal(err)
222 }
223 pins, err := LivePins(opt)
224 if err != nil {
225 t.Fatal(err)
226 }
227 if !pins["gone"] {
228 t.Fatalf("LivePins = %v, want gone pinned", pins)
229 }
230}
231
232// A live dir this package cannot read is an error, not a narrowed pin set:
233// the caller must decline to delete rather than delete a live binary.
234func TestLivePinsErrorsOnUnreadableEntry(t *testing.T) {
235 opt := t.TempDir()
236 if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
237 t.Fatal(err)
238 }
239 // A regular file where a symlink belongs: Readlink returns EINVAL.
240 if err := os.WriteFile(LiveLink(opt, "stray"), []byte("not a link"), 0o644); err != nil {
241 t.Fatal(err)
242 }
243 if _, err := LivePins(opt); err == nil {
244 t.Fatal("LivePins accepted a non-symlink entry; it must refuse")
245 }
246}
247
248func TestLivePinsNoLiveDir(t *testing.T) {
249 pins, err := LivePins(t.TempDir())
250 if err != nil {
251 t.Fatalf("a box with no live dir is not an error: %v", err)
252 }
253 if len(pins) != 0 {
254 t.Errorf("pins = %v, want empty", pins)
255 }
256}
257