311 lines
8.5 KiB
OCaml
311 lines
8.5 KiB
OCaml
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 dispatch_queue = Fsevents.Dispatch_queue.create () in
|
|
List.iter fsevents ~f:(fun f -> Fsevents.start f dispatch_queue);
|
|
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.Dispatch_queue.wait_until_stopped dispatch_queue 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 = "Create"; 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 = "Create"; kind = "File"; path = "$TESTCASE_ROOT/old" }
|
|
> { action = "Rename"; 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";
|
|
(* Delay to allow the event handler callback to catch the exception
|
|
before stopping the watcher. *)
|
|
Unix.sleepf 1.0);
|
|
[%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" } |}];
|
|
(* but relative paths do not *)
|
|
run (fun _ name -> name);
|
|
[%expect
|
|
{|
|
|
> { action = "Create"; kind = "Dir"; path = "$TESTCASE_ROOT/ignored" }
|
|
> { action = "Create"; 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" } |}]
|
|
;;
|