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 linux
2
3package hostfiles
4
5import (
6 "context"
7 "os"
8 "path/filepath"
9 "sync/atomic"
10 "testing"
11 "time"
12
13 "golang.org/x/sys/unix"
14
15 "github.com/carlosframework/platform/internal/release"
16)
17
18// realWatcher returns a working inotify watcher, or skips the test where
19// the kernel will not give one (a shared machine at its per-user limit).
20// CI must not skip: see the plan's Task 7, step 10.
21func realWatcher(t *testing.T, root string, skip func(string, bool) bool) watcher {
22 t.Helper()
23 w, err := newWatcher(root, skip)
24 if err != nil {
25 if os.Getenv("CARLOS_REQUIRE_INOTIFY") != "" {
26 t.Fatalf("inotify is required here: %v", err)
27 }
28 t.Skipf("no inotify instance available: %v", err)
29 }
30 t.Cleanup(func() { w.Close() })
31 return w
32}
33
34// startReal runs the loop on a real watcher with scans and polling off,
35// and returns once the first full scan is done: everything after that is
36// found by an inotify event or not at all.
37func startReal(t *testing.T, s release.Store, dir string) {
38 t.Helper()
39 w := realWatcher(t, dir, nil)
40 p := open(t, s)
41 stop := startRun(t, p, dir, w, Options{Deadline: 50 * time.Millisecond, Scan: time.Hour, Poll: time.Hour})
42 t.Cleanup(func() { stop() })
43}
44
45func TestWatcherPublishesAFileStillHeldOpen(t *testing.T) {
46 s, dir := release.NewDirStore(t.TempDir()), t.TempDir()
47 startReal(t, s, dir)
48 f, err := os.Create(filepath.Join(dir, "open.log"))
49 if err != nil {
50 t.Fatal(err)
51 }
52 defer f.Close() // deliberately not closed before the assertion
53 if _, err := f.WriteString("line one\n"); err != nil {
54 t.Fatal(err)
55 }
56 eventually(t, "open.log published while still open", func() bool {
57 return manifestHas(s, "open.log", sum([]byte("line one\n")))
58 })
59 // The first publication could have come from the file's creation. A
60 // second write through the same descriptor produces no create, no
61 // close and no rename: only IN_MODIFY can announce it.
62 if _, err := f.WriteString("line two\n"); err != nil {
63 t.Fatal(err)
64 }
65 eventually(t, "the second write published, still open", func() bool {
66 return manifestHas(s, "open.log", sum([]byte("line one\nline two\n")))
67 })
68}
69
70func TestWatcherSeesAFileInADirectoryCreatedAfterItStarted(t *testing.T) {
71 s, dir := release.NewDirStore(t.TempDir()), t.TempDir()
72 startReal(t, s, dir)
73 write(t, dir, "new/deep/a.txt", "a")
74 eventually(t, "new/deep/a.txt published", func() bool {
75 return manifestHas(s, "new/deep/a.txt", sum([]byte("a")))
76 })
77 // And a later write inside the new directory is an event, not a scan.
78 time.Sleep(150 * time.Millisecond)
79 write(t, dir, "new/deep/b.txt", "b")
80 eventually(t, "new/deep/b.txt published", func() bool {
81 return manifestHas(s, "new/deep/b.txt", sum([]byte("b")))
82 })
83}
84
85func TestWatcherPublishesARenameAsDeleteAndCreate(t *testing.T) {
86 s, dir := release.NewDirStore(t.TempDir()), t.TempDir()
87 write(t, dir, "tmp", "v")
88 startReal(t, s, dir)
89 if err := os.Rename(filepath.Join(dir, "tmp"), filepath.Join(dir, "final")); err != nil {
90 t.Fatal(err)
91 }
92 eventually(t, "final present and tmp tombstoned", func() bool {
93 m, _, _ := Load(context.Background(), s, pfx)
94 return m.Entries["tmp"].Deleted && m.Entries["final"].SHA256 == sum([]byte("v"))
95 })
96}
97
98func TestWatcherDoesNotWatchASkippedDirectory(t *testing.T) {
99 root := t.TempDir()
100 if err := os.MkdirAll(filepath.Join(root, "cache"), 0o755); err != nil {
101 t.Fatal(err)
102 }
103 w := realWatcher(t, root, func(rel string, isDir bool) bool { return isDir && rel == "cache" })
104 write(t, root, "cache/x", "x")
105 select {
106 case ev := <-w.Events():
107 t.Fatalf("event from a skipped directory: %+v", ev)
108 case <-time.After(150 * time.Millisecond):
109 }
110}
111
112// unwatched builds a watcher with one directory that has no watch and no
113// event on its way: the state an overflow leaves when it swallows that
114// directory's creation. The directory exists before the watcher starts
115// and is excluded while the watcher is built, so the kernel never queues
116// an event that could install the watch later behind the test's back.
117func unwatched(t *testing.T) (w *inotifyWatcher, root string) {
118 t.Helper()
119 root = t.TempDir()
120 if err := os.MkdirAll(filepath.Join(root, "lost"), 0o755); err != nil {
121 t.Fatal(err)
122 }
123 var building atomic.Bool
124 building.Store(true)
125 w = realWatcher(t, root, func(rel string, isDir bool) bool {
126 return building.Load() && isDir && rel == "lost"
127 }).(*inotifyWatcher)
128 building.Store(false)
129 return w, root
130}
131
132// noEventFor fails if an event for rel arrives within 200ms.
133func noEventFor(t *testing.T, w *inotifyWatcher, rel string) {
134 t.Helper()
135 deadline := time.After(200 * time.Millisecond)
136 for {
137 select {
138 case ev := <-w.Events():
139 if ev.rel == rel {
140 t.Fatalf("an event for %s, which has no watch", rel)
141 }
142 case <-deadline:
143 return
144 }
145 }
146}
147
148func eventFor(t *testing.T, w *inotifyWatcher, rel string) {
149 t.Helper()
150 deadline := time.After(2 * time.Second)
151 for {
152 select {
153 case ev := <-w.Events():
154 if ev.rel == rel {
155 return
156 }
157 case <-deadline:
158 t.Fatalf("no event for %s", rel)
159 }
160 }
161}
162
163// Plan round 1 finding 8: a kernel overflow may have swallowed a
164// directory's creation, so coverage is uncertain until Repair has walked
165// the tree again. Only Repair can restore it here: remove the walk from
166// Repair and the last assertion fails.
167func TestWatcherOverflowMarksCoverageUncertainUntilRepaired(t *testing.T) {
168 w, root := unwatched(t)
169 if w.Degraded() {
170 t.Fatal("degraded before anything went wrong")
171 }
172 write(t, root, "lost/before.txt", "b")
173 noEventFor(t, w, "lost/before.txt") // proves the directory really is unwatched
174
175 w.handle(0, unix.IN_Q_OVERFLOW, "")
176 if !w.Degraded() {
177 t.Fatal("an overflow left coverage marked complete")
178 }
179 if err := w.Repair(); err != nil {
180 t.Fatal(err)
181 }
182 if w.Degraded() {
183 t.Fatal("still degraded after a successful repair")
184 }
185 write(t, root, "lost/after.txt", "a")
186 eventFor(t, w, "lost/after.txt")
187}
188
189// Plan round 2 finding 5: a failure that arrives while Repair is walking
190// may concern a directory the walk has already passed. That Repair must
191// not clear it.
192func TestWatcherRepairDoesNotEraseAFailureThatArrivedDuringItsWalk(t *testing.T) {
193 w, _ := unwatched(t)
194 w.handle(0, unix.IN_Q_OVERFLOW, "")
195 orig := afterRepairWalk
196 t.Cleanup(func() { afterRepairWalk = orig })
197 afterRepairWalk = func() {
198 afterRepairWalk = func() {}
199 w.handle(0, unix.IN_Q_OVERFLOW, "") // a second overflow, after the walk
200 }
201 if err := w.Repair(); err != nil {
202 t.Fatal(err)
203 }
204 if !w.Degraded() {
205 t.Fatal("the repair cleared a failure newer than its walk")
206 }
207 if err := w.Repair(); err != nil {
208 t.Fatal(err)
209 }
210 if w.Degraded() {
211 t.Fatal("a repair with nothing new during its walk left the watcher degraded")
212 }
213}
214
215func TestAStoppedWatcherCannotBeRepairedAndClosesItsChannel(t *testing.T) {
216 w := realWatcher(t, t.TempDir(), nil).(*inotifyWatcher)
217 w.Close()
218 select {
219 case _, open := <-w.Events():
220 if open {
221 t.Fatal("an event after Close")
222 }
223 case <-time.After(2 * time.Second):
224 t.Fatal("the channel did not close")
225 }
226 if err := w.Repair(); err == nil {
227 t.Fatal("a stopped watcher reported a successful repair")
228 }
229 if !w.Degraded() {
230 t.Fatal("a stopped watcher is not degraded")
231 }
232}
233