-
Notifications
You must be signed in to change notification settings - Fork 412
/
fsevents_tests.ml
297 lines (264 loc) · 8.5 KB
/
fsevents_tests.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
open Stdune
module Event = Fsevents.Event
module Logger : sig
type t
val create : unit -> t
val printfn : t -> ('a, unit, string, unit) format4 -> 'a
val flush : t -> unit
end = struct
type t = { messages : string Queue.t }
let create () = { messages = Queue.create () }
let printfn t fmt = Printf.ksprintf (fun s -> Queue.push t.messages s) fmt
let flush t =
let rec loop () =
match Queue.pop t.messages with
| None -> ()
| Some s ->
print_endline s;
loop ()
in
loop ()
end
let timeout_thread ~wait f =
let spawn () =
Thread.delay wait;
f ()
in
let (_ : Thread.t) = Thread.create spawn () in
()
let start_filename = ".dune_fsevents_start"
let end_filename = ".dune_fsevents_end"
let emit_start dir =
ignore (Fpath.mkdir_p dir);
Io.String_path.write_file (Filename.concat dir start_filename) ""
let emit_stop dir =
ignore (Fpath.mkdir_p dir);
Io.String_path.write_file (Filename.concat dir end_filename) ""
let test f =
let cv = Condition.create () in
let mutex = Mutex.create () in
let finished = ref false in
let finish () =
Mutex.lock mutex;
finished := true;
Condition.signal cv;
Mutex.unlock mutex
in
timeout_thread ~wait:3.0 (fun () ->
Mutex.lock mutex;
if not !finished then (
Format.eprintf "Test timed out@.";
finished := true;
Condition.signal cv);
Mutex.unlock mutex);
let test () =
let dir = Temp.create Dir ~prefix:"fsevents_dune" ~suffix:"" in
let old = Sys.getcwd () in
Sys.chdir (Path.to_string dir);
Exn.protect
~f:(fun () -> f finish)
~finally:(fun () ->
Sys.chdir old;
Temp.destroy Dir dir)
in
let (_ : Thread.t) = Thread.create test () in
Mutex.lock mutex;
while not !finished do
Condition.wait cv mutex
done;
Mutex.unlock mutex
let print_event ~logger ~cwd e =
let dyn =
let open Dyn in
record
[ ("action", Event.dyn_of_action (Event.action e))
; ("kind", Event.dyn_of_kind (Event.kind e))
; ( "path"
, string
(let path = Event.path e in
match String.drop_prefix ~prefix:cwd path with
| None -> path
| Some p -> "$TESTCASE_ROOT" ^ p) )
]
in
Logger.printfn logger "> %s" (Dyn.to_string dyn)
let make_callback sync ~f =
(* hack to skip the first event if it's creating the temp dir *)
let state = ref `Looking_start in
fun events ->
let is_marker event filename =
Event.kind event = File
&& Filename.basename (Event.path event) = filename
&& Event.action event = Create
in
let events =
List.fold_left events ~init:[] ~f:(fun acc event ->
match !state with
| `Looking_start ->
if is_marker event start_filename then (
state := `Keep;
sync#start);
acc
| `Finish -> acc
| `Keep ->
if is_marker event end_filename then (
state := `Finish;
sync#stop;
acc)
else event :: acc)
in
match events with
| [] -> ()
| _ -> List.rev events |> List.iter ~f:(f ~logger:sync#logger)
type test_config =
{ on_event : logger:Logger.t -> Event.t -> unit
; exclusion_paths : string list
; dir : string
}
let default_test_config cwd =
{ on_event = print_event ~cwd; dir = cwd; exclusion_paths = [] }
let test_with_multiple_fsevents ~setup ~test:f =
test (fun finish ->
let cwd = Sys.getcwd () in
let make_sync t config =
let logger = Logger.create () in
object
val mutable started = false
val mutable stopped = false
method logger = logger
method started = started
method stopped = stopped
method start = started <- true
method stop =
stopped <- true;
Fsevents.stop (Option.value_exn !t)
method emit_start = if not started then emit_start config.dir
method emit_stop = if not stopped then emit_stop config.dir
end
in
let configs = setup ~cwd (default_test_config cwd) in
let fsevents, syncs =
List.map configs ~f:(fun config ->
let t = ref None in
let sync = make_sync t config in
let res =
Fsevents.create ~paths:[ config.dir ] ~latency:0.0
~f:(make_callback sync ~f:config.on_event)
in
(match config.exclusion_paths with
| [] -> ()
| paths ->
(* apple doesn't like [paths] empty *)
Fsevents.set_exclusion_paths res ~paths);
t := Some res;
(res, sync))
|> List.unzip
in
let runloop = Fsevents.RunLoop.in_current_thread () in
List.iter fsevents ~f:(fun f -> Fsevents.start f runloop);
let (t : Thread.t) =
Thread.create
(fun () ->
let rec await ~emit ~continue = function
| [] -> ()
| xs ->
List.iter xs ~f:emit;
Unix.sleepf 0.2;
await ~emit ~continue (List.filter xs ~f:continue)
in
await
~emit:(fun sync -> sync#emit_start)
~continue:(fun sync -> not sync#started)
syncs;
f ();
await
~emit:(fun sync -> sync#emit_stop)
~continue:(fun sync -> not sync#stopped)
syncs)
()
in
(match Fsevents.RunLoop.run_current_thread runloop with
| Error Exit -> print_endline "[EXIT]"
| Error _ -> assert false
| Ok () -> ());
Thread.join t;
List.iter syncs ~f:(fun c -> Logger.flush c#logger);
finish ())
let test_with_operations ?on_event ?exclusion_paths f =
test_with_multiple_fsevents ~test:f ~setup:(fun ~cwd config ->
let config =
match exclusion_paths with
| None -> config
| Some f -> { config with exclusion_paths = f cwd }
in
[ (match on_event with
| None -> config
| Some on_event -> { config with on_event })
])
let%expect_test "file create event" =
test_with_operations (fun () -> Io.String_path.write_file "./file" "foobar");
[%expect
{|
> { action = "Unknown"; kind = "File"; path = "$TESTCASE_ROOT/file" } |}]
let%expect_test "dir create event" =
test_with_operations (fun () -> ignore (Fpath.mkdir "./blahblah"));
[%expect
{|
> { action = "Create"; kind = "Dir"; path = "$TESTCASE_ROOT/blahblah" } |}]
let%expect_test "move file" =
test_with_operations (fun () ->
Io.String_path.write_file "old" "foobar";
Unix.rename "old" "new");
[%expect
{|
> { action = "Unknown"; kind = "File"; path = "$TESTCASE_ROOT/old" }
> { action = "Unknown"; kind = "File"; path = "$TESTCASE_ROOT/new" } |}]
let%expect_test "raise inside callback" =
test_with_operations
~on_event:(fun ~logger _ ->
Logger.printfn logger "exiting.";
raise Exit)
(fun () ->
Io.String_path.write_file "old" "foobar";
Io.String_path.write_file "old" "foobar");
[%expect {|
[EXIT]
exiting. |}]
let%expect_test "set exclusion paths" =
let run paths =
let ignored = "ignored" in
test_with_operations
~exclusion_paths:(fun cwd -> [ paths cwd ignored ])
(fun () ->
let (_ : Fpath.mkdir_p_result) = Fpath.mkdir_p ignored in
Io.String_path.write_file (Filename.concat ignored "old") "foobar")
in
(* absolute paths work *)
run Filename.concat;
[%expect
{|
> { action = "Create"; kind = "Dir"; path = "$TESTCASE_ROOT/ignored" }
> { action = "Unknown"; kind = "File"; path = "$TESTCASE_ROOT/ignored/old" } |}];
(* but relative paths do not *)
run (fun _ name -> name);
[%expect
{|
> { action = "Create"; kind = "Dir"; path = "$TESTCASE_ROOT/ignored" }
> { action = "Unknown"; kind = "File"; path = "$TESTCASE_ROOT/ignored/old" } |}]
let%expect_test "multiple fsevents" =
test_with_multiple_fsevents
~setup:(fun ~cwd config ->
let create path =
let dir = Filename.concat cwd path in
ignore (Fpath.mkdir dir);
{ config with dir }
in
[ create "foo"; create "bar" ])
~test:(fun () ->
Io.String_path.write_file "foo/file" "";
Io.String_path.write_file "bar/file" "";
Io.String_path.write_file "xxx" "" (* this one is ignored *));
[%expect
{|
> { action = "Create"; kind = "File"; path = "$TESTCASE_ROOT/foo/file" }
> { action = "Create"; kind = "File"; path = "$TESTCASE_ROOT/bar/file" } |}]