carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build unix
2
3package hostfiles
4
5import (
6 "context"
7 "io"
8 "net"
9 "os"
10 "path/filepath"
11 "reflect"
12 "syscall"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/release"
17)
18
19// within fails the test if fn has not returned in two seconds: the way a
20// blocked open shows up.
21func within(t *testing.T, what string, fn func()) {
22 t.Helper()
23 done := make(chan struct{})
24 go func() { defer close(done); fn() }()
25 select {
26 case <-done:
27 case <-time.After(2 * time.Second):
28 t.Fatalf("%s did not return: something blocked", what)
29 }
30}
31
32// swapAt arranges for fn to run once, at the nth pause between a look and
33// an open (1 is the first).
34func swapAt(t *testing.T, n int, fn func()) {
35 t.Helper()
36 orig, calls := betweenLookAndOpen, 0
37 t.Cleanup(func() { betweenLookAndOpen = orig })
38 betweenLookAndOpen = func() {
39 calls++
40 if calls == n {
41 fn()
42 }
43 }
44}
45
46func tree(t *testing.T, dir string) *dirHandle {
47 t.Helper()
48 d, err := openTree(dir)
49 if err != nil {
50 t.Fatal(err)
51 }
52 t.Cleanup(func() { d.Close() })
53 return d
54}
55
56func fifo(t *testing.T, path string) {
57 t.Helper()
58 if err := syscall.Mkfifo(path, 0o600); err != nil {
59 t.Skipf("no FIFOs here: %v", err)
60 }
61}
62
63// Review Focus 2: special files are skipped, never opened for reading.
64func TestSyncDirSkipsAFIFO(t *testing.T) {
65 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
66 write(t, src, "real.txt", "r")
67 fifo(t, filepath.Join(src, "pipe"))
68 p := open(t, s)
69 var st SyncStats
70 within(t, "SyncDir", func() { st, _ = SyncDir(ctx, p, src, nil) })
71 if st.Published != 1 || !reflect.DeepEqual(st.NotRegular, []string{"pipe"}) {
72 t.Fatalf("stats = %+v", st)
73 }
74}
75
76func TestSyncDirSkipsASocket(t *testing.T) {
77 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
78 l, err := net.Listen("unix", filepath.Join(src, "sock"))
79 if err != nil {
80 t.Skipf("no Unix sockets here: %v", err)
81 }
82 defer l.Close()
83 p := open(t, s)
84 var st SyncStats
85 within(t, "SyncDir", func() { st, _ = SyncDir(ctx, p, src, nil) })
86 if !reflect.DeepEqual(st.NotRegular, []string{"sock"}) || len(p.Paths()) != 0 {
87 t.Fatalf("stats = %+v, paths = %v", st, p.Paths())
88 }
89}
90
91func TestSyncDirSkipsADevice(t *testing.T) {
92 if os.Getuid() != 0 {
93 t.Skip("making a device node needs root")
94 }
95 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
96 if err := syscall.Mknod(filepath.Join(src, "null"), syscall.S_IFCHR|0o600, 1<<8|3); err != nil {
97 t.Skipf("mknod: %v", err)
98 }
99 p := open(t, s)
100 st, err := SyncDir(ctx, p, src, nil)
101 if err != nil || !reflect.DeepEqual(st.NotRegular, []string{"null"}) {
102 t.Fatalf("stats = %+v, err = %v", st, err)
103 }
104}
105
106// A symlinked directory is never descended into, wherever it points.
107func TestSyncDirDoesNotDescendASymlinkedDirectory(t *testing.T) {
108 ctx, s, src, outside := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir(), t.TempDir()
109 if err := os.WriteFile(filepath.Join(outside, "secret"), []byte("s"), 0o600); err != nil {
110 t.Fatal(err)
111 }
112 write(t, src, "inside/own", "o")
113 for link, target := range map[string]string{"out": outside, "in": "inside"} {
114 if err := os.Symlink(target, filepath.Join(src, link)); err != nil {
115 t.Fatal(err)
116 }
117 }
118 p := open(t, s)
119 st, err := SyncDir(ctx, p, src, nil)
120 if err != nil {
121 t.Fatal(err)
122 }
123 if got := p.Paths(); !reflect.DeepEqual(got, []string{"inside/own"}) {
124 t.Fatalf("Paths = %v", got)
125 }
126 if !reflect.DeepEqual(st.NotRegular, []string{"in", "out"}) {
127 t.Fatalf("NotRegular = %v", st.NotRegular)
128 }
129}
130
131// A regular file swapped for a FIFO after it was inspected must neither
132// block the open nor be read.
133func TestReadStableDoesNotBlockOnAFIFOSwappedIn(t *testing.T) {
134 dir := t.TempDir()
135 write(t, dir, "f", "regular")
136 d := tree(t, dir)
137 swapAt(t, 1, func() {
138 full := filepath.Join(dir, "f")
139 if err := os.Remove(full); err != nil {
140 t.Error(err)
141 }
142 if err := syscall.Mkfifo(full, 0o600); err != nil {
143 t.Error(err)
144 }
145 })
146 var body []byte
147 var state fileState
148 within(t, "readStable", func() { body, _, state, _ = readStable(d, "f") })
149 if state != fileNotRegular || body != nil {
150 t.Fatalf("state = %v, %d bytes; want fileNotRegular and nothing read", state, len(body))
151 }
152}
153
154// A directory swapped for a FIFO between being listed and being entered
155// must not hang the scan.
156func TestSyncDirDoesNotBlockOnADirectorySwappedForAFIFO(t *testing.T) {
157 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
158 write(t, src, "d/child", "c")
159 write(t, src, "z.txt", "z")
160 swapAt(t, 1, func() { // the first pause is before entering "d"
161 if err := os.RemoveAll(filepath.Join(src, "d")); err != nil {
162 t.Error(err)
163 }
164 if err := syscall.Mkfifo(filepath.Join(src, "d"), 0o600); err != nil {
165 t.Error(err)
166 }
167 })
168 p := open(t, s)
169 var st SyncStats
170 var err error
171 within(t, "SyncDir", func() { st, err = SyncDir(ctx, p, src, nil) })
172 if err != nil {
173 t.Fatal(err)
174 }
175 if !reflect.DeepEqual(st.NotRegular, []string{"d"}) || !reflect.DeepEqual(p.Paths(), []string{"z.txt"}) {
176 t.Fatalf("stats = %+v, paths = %v", st, p.Paths())
177 }
178}
179
180// Plan round 2, finding 2: a file that still exists is never tombstoned
181// just because it could not be read.
182func TestSyncDirKeepsAnUnreadableFilesPublishedEntry(t *testing.T) {
183 if os.Getuid() == 0 {
184 t.Skip("root can read anything")
185 }
186 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
187 write(t, src, "locked", "v1")
188 p := open(t, s)
189 if _, err := SyncDir(ctx, p, src, nil); err != nil {
190 t.Fatal(err)
191 }
192 commit(t, p)
193 if err := os.Chmod(filepath.Join(src, "locked"), 0); err != nil {
194 t.Fatal(err)
195 }
196 st, err := SyncDir(ctx, p, src, nil)
197 if err != nil {
198 t.Fatal(err)
199 }
200 if st.Deleted != 0 || p.Dirty() || !reflect.DeepEqual(st.Unreadable, []string{"locked"}) {
201 t.Fatalf("stats = %+v dirty = %v", st, p.Dirty())
202 }
203}
204
205// A regular file replaced by another regular file between the look and
206// the open is neither read nor declared gone: the caller asks again.
207func TestReadStableAsksAgainWhenARegularFileIsReplacedMidLook(t *testing.T) {
208 dir := t.TempDir()
209 write(t, dir, "f", "v1")
210 d := tree(t, dir)
211 swapAt(t, 1, func() {
212 tmp := filepath.Join(dir, "f.new")
213 if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil {
214 t.Error(err)
215 }
216 if err := os.Rename(tmp, filepath.Join(dir, "f")); err != nil {
217 t.Error(err)
218 }
219 })
220 body, _, state, err := readStable(d, "f")
221 if err != nil || state != fileRetry || body != nil {
222 t.Fatalf("state = %v, body = %q, err = %v; want fileRetry and nothing read", state, body, err)
223 }
224 body, _, state, err = readStable(d, "f")
225 if err != nil || state != fileOK || string(body) != "v2" {
226 t.Fatalf("second look: state = %v, body = %q, err = %v", state, body, err)
227 }
228}
229
230// And through a scan: the entry the manifest already has is kept.
231func TestSyncDirKeepsAnEntryWhoseFileWasReplacedMidLook(t *testing.T) {
232 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
233 write(t, src, "f", "v1")
234 p := open(t, s)
235 if _, err := SyncDir(ctx, p, src, nil); err != nil {
236 t.Fatal(err)
237 }
238 commit(t, p)
239 swapAt(t, 1, func() {
240 tmp := filepath.Join(src, "f.new")
241 if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil {
242 t.Error(err)
243 }
244 if err := os.Rename(tmp, filepath.Join(src, "f")); err != nil {
245 t.Error(err)
246 }
247 })
248 st, err := SyncDir(ctx, p, src, nil)
249 if err != nil || st.Deleted != 0 || p.Dirty() {
250 t.Fatalf("stats = %+v dirty = %v err = %v; the replaced file must be left for the next scan", st, p.Dirty(), err)
251 }
252 if _, err := SyncDir(ctx, p, src, nil); err != nil || !p.Dirty() {
253 t.Fatalf("the next scan did not pick the replacement up: dirty = %v err = %v", p.Dirty(), err)
254 }
255}
256
257// Publish is an installation's record of its directory: a file it could
258// not read would be missing from it, so it commits nothing and says so.
259func TestPublishRefusesADirectoryWithAnUnreadableFile(t *testing.T) {
260 if os.Getuid() == 0 {
261 t.Skip("root can read anything")
262 }
263 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
264 write(t, src, "ok", "v1")
265 write(t, src, "locked", "v1")
266 if err := os.Chmod(filepath.Join(src, "locked"), 0); err != nil {
267 t.Fatal(err)
268 }
269 if _, st, err := Publish(ctx, s, pfx, src, nil); err == nil || !reflect.DeepEqual(st.Unreadable, []string{"locked"}) {
270 t.Fatalf("err = %v, stats = %+v", err, st)
271 }
272 if _, found, err := Load(ctx, s, pfx); found || err != nil {
273 t.Fatalf("a manifest was committed: found = %v, err = %v", found, err)
274 }
275}
276
277// replaceAt swaps src/f for new content at the n-th look-to-open window.
278func replaceAt(t *testing.T, n int, src string) {
279 t.Helper()
280 swapAt(t, n, func() {
281 tmp := filepath.Join(src, "f.new")
282 if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil {
283 t.Error(err)
284 }
285 if err := os.Rename(tmp, filepath.Join(src, "f")); err != nil {
286 t.Error(err)
287 }
288 })
289}
290
291// A file replaced mid-look is reported, not silently left out.
292func TestSyncDirReportsAFileReplacedMidLook(t *testing.T) {
293 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
294 write(t, src, "f", "v1")
295 replaceAt(t, 1, src)
296 st, err := SyncDir(ctx, open(t, s), src, nil)
297 if err != nil || !reflect.DeepEqual(st.Retry, []string{"f"}) {
298 t.Fatalf("stats = %+v err = %v, want Retry [f]", st, err)
299 }
300}
301
302// A first install whose only file was replaced mid-look would commit a
303// manifest without it.
304func TestPublishRefusesADirectoryWithAFileReplacedMidLook(t *testing.T) {
305 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
306 write(t, src, "ok", "v1")
307 write(t, src, "f", "v1")
308 replaceAt(t, 1, src) // "f" sorts before "ok"
309 if _, st, err := Publish(ctx, s, pfx, src, nil); err == nil || !reflect.DeepEqual(st.Retry, []string{"f"}) {
310 t.Fatalf("err = %v, stats = %+v", err, st)
311 }
312 if _, found, err := Load(ctx, s, pfx); found || err != nil {
313 t.Fatalf("a manifest was committed: found = %v, err = %v", found, err)
314 }
315}
316
317// tearAt makes the read of src/f see the file grow while it is read.
318func tearAt(t *testing.T, src string) {
319 t.Helper()
320 orig := readAll
321 t.Cleanup(func() { readAll = orig })
322 readAll = func(r io.Reader) ([]byte, error) {
323 b, err := orig(r)
324 f, oerr := os.OpenFile(filepath.Join(src, "f"), os.O_APPEND|os.O_WRONLY, 0)
325 if oerr != nil {
326 t.Error(oerr)
327 return b, err
328 }
329 f.Write([]byte("more"))
330 f.Close()
331 return b, err
332 }
333}
334
335func TestSyncDirReportsATornRead(t *testing.T) {
336 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
337 write(t, src, "f", "v1")
338 tearAt(t, src)
339 st, err := SyncDir(ctx, open(t, s), src, nil)
340 if err != nil || !reflect.DeepEqual(st.Torn, []string{"f"}) {
341 t.Fatalf("stats = %+v err = %v, want Torn [f]", st, err)
342 }
343}
344
345// Publish is an install-time record: a torn image is not committed.
346func TestPublishRefusesATornRead(t *testing.T) {
347 ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
348 write(t, src, "f", "v1")
349 tearAt(t, src)
350 if _, st, err := Publish(ctx, s, pfx, src, nil); err == nil || !reflect.DeepEqual(st.Torn, []string{"f"}) {
351 t.Fatalf("err = %v, stats = %+v", err, st)
352 }
353 if _, found, err := Load(ctx, s, pfx); found || err != nil {
354 t.Fatalf("a manifest was committed: found = %v, err = %v", found, err)
355 }
356}
357