From 81a12973003757b22525f07afa72cfce9fdd2c3b Mon Sep 17 00:00:00 2001 From: Jason Axelson Date: Sat, 12 Oct 2019 12:36:31 -0700 Subject: [PATCH] Add tests for file watcher Also restructure Cortex.FileWatcher to better support testing --- lib/cortex/file_watcher.ex | 39 ++++++++++++++++----- test/cortex/file_watcher_test.exs | 58 +++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/lib/cortex/file_watcher.ex b/lib/cortex/file_watcher.ex index 554cde0..2e7fb3d 100644 --- a/lib/cortex/file_watcher.ex +++ b/lib/cortex/file_watcher.ex @@ -8,30 +8,50 @@ defmodule Cortex.FileWatcher do use GenServer @watched_dirs ["lib/", "test/", "apps/"] - @throttle_timeout_ms 100 + @default_throttle_timeout_ms 100 + @default_file_changed_receiver Controller defmodule State do - defstruct [:watcher_pid, :file_events, :throttle_timer] + defstruct [ + :watcher_pid, + :file_events, + :throttle_timer, + :throttle_timeout_ms, + :file_changed_receiver + ] end ########################################## # Public API ########################################## - def start_link do - GenServer.start_link(__MODULE__, []) + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, opts) end ########################################## # GenServer Callbacks ########################################## - def init(_) do + def init(opts) do + throttle_timeout_ms = Keyword.get(opts, :throttle_timeout_ms, @default_throttle_timeout_ms) + + file_changed_receiver = + Keyword.get(opts, :file_changed_receiver, @default_file_changed_receiver) + {:ok, watcher_pid} = FileSystem.start_link(dirs: watched_dirs()) FileSystem.subscribe(watcher_pid) - {:ok, %State{watcher_pid: watcher_pid, throttle_timer: nil, file_events: %{}}} + initial_state = %State{ + watcher_pid: watcher_pid, + throttle_timer: nil, + throttle_timeout_ms: throttle_timeout_ms, + file_changed_receiver: file_changed_receiver, + file_events: %{} + } + + {:ok, initial_state} end def handle_info( @@ -53,10 +73,10 @@ defmodule Cortex.FileWatcher do end def handle_info(:throttle_timer_complete, state) do - %State{file_events: file_events} = state + %State{file_events: file_events, file_changed_receiver: file_changed_receiver} = state Enum.each(file_events, fn {path, file_type} -> - GenServer.cast(Controller, {:file_changed, file_type, path}) + GenServer.cast(file_changed_receiver, {:file_changed, file_type, path}) end) {:noreply, %State{state | file_events: %{}, throttle_timer: nil}} @@ -73,7 +93,8 @@ defmodule Cortex.FileWatcher do ########################################## defp maybe_update_throttle_timer(%State{throttle_timer: nil} = state) do - throttle_timer = Process.send_after(self(), :throttle_timer_complete, @throttle_timeout_ms) + %State{throttle_timeout_ms: throttle_timeout_ms} = state + throttle_timer = Process.send_after(self(), :throttle_timer_complete, throttle_timeout_ms) %State{state | throttle_timer: throttle_timer} end diff --git a/test/cortex/file_watcher_test.exs b/test/cortex/file_watcher_test.exs index 7da43e1..0ca8bc3 100644 --- a/test/cortex/file_watcher_test.exs +++ b/test/cortex/file_watcher_test.exs @@ -3,9 +3,67 @@ defmodule Cortex.FileWatcherTest do alias Cortex.FileWatcher + @throttle_timeout_ms 10 + test "file_type" do assert FileWatcher.file_type("test/some_file.exs") == :test assert FileWatcher.file_type("lib/some_file.ex") == :lib assert FileWatcher.file_type("lib/.#some_file.ex") == :unknown end + + test "with one file event received, file changed is sent once" do + {watcher, watcher_pid} = start_file_watcher() + + path = "test/some_file.exs" + + send_file_event(watcher, watcher_pid, path, [:created, :modified]) + + assert_receive {:"$gen_cast", {:file_changed, :test, path}} + refute_receive {:"$gen_cast", {:file_changed, :lib, _}} + end + + test "with one path event multiple times, file changed is sent once" do + {watcher, watcher_pid} = start_file_watcher() + + path = "lib/some_file.ex" + + send_file_event(watcher, watcher_pid, path, [:inodemetamod, :modified]) + send_file_event(watcher, watcher_pid, path, [:inodemetamod, :modified]) + send_file_event(watcher, watcher_pid, path, [:inodemetamod, :modified]) + + assert_receive {:"$gen_cast", {:file_changed, :lib, path}} + refute_receive {:"$gen_cast", {:file_changed, :lib, _}} + end + + test "with multiple paths events sent multiple times, file changed is sent once per path" do + {watcher, watcher_pid} = start_file_watcher() + + path1 = "lib/some_file.ex" + path2 = "lib/another_file.ex" + + send_file_event(watcher, watcher_pid, path1, [:inodemetamod, :modified]) + send_file_event(watcher, watcher_pid, path1, [:inodemetamod, :modified]) + Process.sleep(3) + send_file_event(watcher, watcher_pid, path2, [:created, :modified]) + send_file_event(watcher, watcher_pid, path1, [:inodemetamod, :modified]) + + assert_receive {:"$gen_cast", {:file_changed, :lib, path1}} + assert_receive {:"$gen_cast", {:file_changed, :lib, path2}} + refute_receive {:"$gen_cast", {:file_changed, :lib, _}} + end + + defp start_file_watcher do + watcher = + start_supervised!( + {FileWatcher, file_changed_receiver: self(), throttle_timeout_ms: @throttle_timeout_ms} + ) + + %FileWatcher.State{watcher_pid: watcher_pid} = :sys.get_state(watcher) + {watcher, watcher_pid} + end + + defp send_file_event(watcher, watcher_pid, path, events) do + message = {:file_event, watcher_pid, {path, events}} + send(watcher, message) + end end