Skip to content

Commit 999ead8

Browse files
authored
Add TmpDir.await_cleanup/1 for reliable test synchronization (#133)
* Add TmpDir.await_cleanup/1 for reliable test synchronization Add a GenServer call that waits for cleanup to complete after a monitored process exits. If the process is still monitored, the reply is deferred until the :DOWN message is processed. This eliminates the race condition where :DOWN delivery to the GenServer could be delayed. * Stub Diff.HexMock in tests that trigger diff generation Tests that return {:error, :not_found} from get_metadata trigger async diff generation via handle_info. Without a HexMock stub, the LiveView process crashes with Mox.UnexpectedCallError after the test exits.
1 parent de2df56 commit 999ead8

4 files changed

Lines changed: 42 additions & 18 deletions

File tree

lib/diff/tmp_dir.ex

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,33 +32,57 @@ defmodule Diff.TmpDir do
3232
dir
3333
end
3434

35+
def await_cleanup(pid) do
36+
GenServer.call(__MODULE__, {:await_cleanup, pid}, 5000)
37+
end
38+
3539
defp track(path) do
3640
pid = self()
3741
:ets.insert(@table, {pid, path})
38-
GenServer.cast(__MODULE__, {:monitor, pid})
42+
GenServer.call(__MODULE__, {:monitor, pid})
3943
end
4044

4145
@impl true
4246
def init(_opts) do
4347
Process.flag(:trap_exit, true)
4448
:ets.new(@table, [:named_table, :duplicate_bag, :public])
45-
{:ok, %{monitors: MapSet.new()}}
49+
{:ok, %{monitors: MapSet.new(), waiters: %{}}}
4650
end
4751

4852
@impl true
49-
def handle_cast({:monitor, pid}, state) do
53+
def handle_call({:monitor, pid}, _from, state) do
5054
if pid in state.monitors do
51-
{:noreply, state}
55+
{:reply, :ok, state}
5256
else
5357
Process.monitor(pid)
54-
{:noreply, %{state | monitors: MapSet.put(state.monitors, pid)}}
58+
{:reply, :ok, %{state | monitors: MapSet.put(state.monitors, pid)}}
59+
end
60+
end
61+
62+
@impl true
63+
def handle_call({:await_cleanup, pid}, from, state) do
64+
if pid in state.monitors do
65+
waiters = Map.update(state.waiters, pid, [from], &[from | &1])
66+
{:noreply, %{state | waiters: waiters}}
67+
else
68+
{:reply, :ok, state}
5569
end
5670
end
5771

5872
@impl true
5973
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
6074
cleanup_pid(pid)
61-
{:noreply, %{state | monitors: MapSet.delete(state.monitors, pid)}}
75+
76+
for from <- Map.get(state.waiters, pid, []) do
77+
GenServer.reply(from, :ok)
78+
end
79+
80+
{:noreply,
81+
%{
82+
state
83+
| monitors: MapSet.delete(state.monitors, pid),
84+
waiters: Map.delete(state.waiters, pid)
85+
}}
6286
end
6387

6488
@impl true

test/diff/tmp_dir_test.exs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ defmodule Diff.TmpDirTest do
2222
send(test_pid, {:paths, file, dir})
2323
end)
2424

25-
ref = Process.monitor(task_pid)
2625
assert_receive {:paths, file, dir}
27-
wait_for_cleanup(task_pid, ref)
26+
Diff.TmpDir.await_cleanup(task_pid)
2827

2928
refute File.exists?(file)
3029
refute File.exists?(dir)
@@ -42,9 +41,8 @@ defmodule Diff.TmpDirTest do
4241
raise "crash"
4342
end)
4443

45-
ref = Process.monitor(task_pid)
4644
assert_receive {:paths, file, dir}
47-
wait_for_cleanup(task_pid, ref)
45+
Diff.TmpDir.await_cleanup(task_pid)
4846

4947
refute File.exists?(file)
5048
refute File.exists?(dir)
@@ -65,9 +63,8 @@ defmodule Diff.TmpDirTest do
6563
send(test_pid, {:paths, paths})
6664
end)
6765

68-
ref = Process.monitor(task_pid)
6966
assert_receive {:paths, paths}
70-
wait_for_cleanup(task_pid, ref)
67+
Diff.TmpDir.await_cleanup(task_pid)
7168

7269
for {file, dir} <- paths do
7370
refute File.exists?(file)
@@ -82,10 +79,4 @@ defmodule Diff.TmpDirTest do
8279
assert File.exists?(file)
8380
assert File.dir?(dir)
8481
end
85-
86-
defp wait_for_cleanup(task_pid, ref) do
87-
assert_receive {:DOWN, ^ref, :process, ^task_pid, _}, 5000
88-
# Sync with the GenServer to ensure the :DOWN cleanup has been processed
89-
:sys.get_state(Diff.TmpDir)
90-
end
9182
end

test/diff_web/integration_test.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ defmodule DiffWeb.IntegrationTest do
3131
{:error, :not_found}
3232
end)
3333

34+
Diff.HexMock
35+
|> stub(:diff, fn "phoenix", "1.4.5", "1.4.9" -> :error end)
36+
3437
{:ok, _view, html} = live(conn, "/diff/phoenix/1.4.5..")
3538

3639
# Should show generating state since we're resolving to latest version
@@ -46,6 +49,9 @@ defmodule DiffWeb.IntegrationTest do
4649
{:error, :not_found}
4750
end)
4851

52+
Diff.HexMock
53+
|> stub(:diff, fn "nonexistent", "1.0.0", "2.0.0" -> :error end)
54+
4955
{:ok, _view, html} = live(conn, "/diff/nonexistent/1.0.0..2.0.0")
5056

5157
assert html =~ "Generating diffs"

test/diff_web/live/diff_live_view_test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ defmodule DiffWeb.DiffLiveViewTest do
5454
{:error, :not_found}
5555
end)
5656

57+
Diff.HexMock
58+
|> stub(:diff, fn "phoenix", "1.4.5", "1.4.9" -> :error end)
59+
5760
{:ok, _view, html} = live(conn, "/diff/phoenix/1.4.5..1.4.9")
5861

5962
# Should show generating state when metadata is not found

0 commit comments

Comments
 (0)