add helper to assert that a LiveView will receive a message - #4242
add helper to assert that a LiveView will receive a message#4242SteffenDE wants to merge 3 commits into
Conversation
Uses OTP 27's trace sessions to trace messages received by the LiveView.
|
Yeah, I think it could be part of Elixir, but I am not sure I like the name "assert_will_receive". You may also want to receive multiple messages within a single trace/function. Maybe an API should be more like: We call it |
|
Or perhaps |
|
Another option is: But most usages would likely be: That seems to be the one with most potential for growth. |
|
I like it. The only concern I have in mind is in which process the function should run. Right now, the PR prefixes messages with a ref and sends them to the test process (and also flushes other messages with that ref). If we want to support calling regular |
|
I was thinking that we would use a separate process to proxy messages to the test inbox. The only issue is that messages will appear out of order, but that’s probably fine if documented. |
diff --git a/lib/ex_unit/lib/ex_unit/assertions.ex b/lib/ex_unit/lib/ex_unit/assertions.ex
index 0f44e52f8..dcce18b53 100644
--- a/lib/ex_unit/lib/ex_unit/assertions.ex
+++ b/lib/ex_unit/lib/ex_unit/assertions.ex
@@ -1076,6 +1076,83 @@ defp refute_receive_clause(pattern, failure_message) do
end
end
+ @doc """
+ Traces `pid` while `fun` runs, delivering trace messages to the calling process.
+
+ This is a thin wrapper around Erlang's [trace sessions](`:trace`) that lets you
+ assert on the internal behaviour of a process — the messages it sends and
+ receives, the functions it calls, and so on — using the regular
+ `assert_receive/3` and `assert_received/2` assertions.
+
+ Tracing is enabled before `fun` is invoked and disabled once it returns. The
+ value returned by `fun` is returned.
+
+ `flags` is a list of trace flags. In addition to the process trace flags
+ documented in `:trace.process/4` (such as `:send`, `:receive`, and `:procs`),
+ it accepts `{:call, {module, function, arity}}` entries to trace calls to the
+ given function.
+
+ Trace messages are delivered to the calling process in the shapes documented
+ in `:trace.process/4`, for example `{:trace, pid, :receive, message}` or
+ `{:trace, pid, :call, {module, function, args}}`. Because they are delivered
+ asynchronously, prefer `assert_receive/3` over `assert_received/2` when the
+ traced activity is triggered in another process.
+
+ Trace messages left in the mailbox once `fun` returns are discarded, so
+ assertions should be made inside `fun`.
+
+ > #### Requirements {: .info}
+ >
+ > This function relies on trace sessions, which require Erlang/OTP 27 or later.
+
+ ## Examples
+
+ Asserting that a process receives a message:
+
+ trace(pid, [:receive], fn ->
+ send(pid, {:event, "click"})
+ assert_receive {:trace, ^pid, :receive, {:event, "click"}}
+ end)
+
+ Tracing function calls (`call: {module, function, arity}` is keyword syntax
+ for `{:call, {module, function, arity}}`):
+
+ trace(pid, [call: {Map, :get, 2}], fn ->
+ run_request()
+ assert_receive {:trace, ^pid, :call, {Map, :get, [_map, :key]}}
+ end)
+
+ """
+ @doc since: "1.21.0"
+ def trace(pid, flags, fun)
+ when is_pid(pid) and is_list(flags) and is_function(fun, 0) do
+ {call_patterns, process_flags} = Enum.split_with(flags, &match?({:call, _}, &1))
+ process_flags = if call_patterns == [], do: process_flags, else: [:call | process_flags]
+
+ session = :trace.session_create(:ex_unit_trace, self(), [])
+
+ try do
+ Enum.each(call_patterns, fn {:call, mfa} ->
+ :trace.function(session, mfa, true, [:local])
+ end)
+
+ :trace.process(session, pid, true, process_flags)
+ fun.()
+ after
+ :trace.session_destroy(session)
+ flush_trace(pid)
+ end
+ end
+
+ defp flush_trace(pid) do
+ receive do
+ {:trace, ^pid, _, _} -> flush_trace(pid)
+ {:trace, ^pid, _, _, _} -> flush_trace(pid)
+ after
+ 0 -> :ok
+ end
+ end
+
@doc """
Asserts `value1` and `value2` are not within `delta`.
diff --git a/lib/ex_unit/test/ex_unit/assertions_test.exs b/lib/ex_unit/test/ex_unit/assertions_test.exs
index 52192206d..74dcc6880 100644
--- a/lib/ex_unit/test/ex_unit/assertions_test.exs
+++ b/lib/ex_unit/test/ex_unit/assertions_test.exs
@@ -615,6 +615,50 @@ test "assert received does not leak external variables used in guards" do
:world = world
end
+ describe "trace" do
+ test "delivers receive trace messages to the calling process" do
+ pid = spawn_link(fn -> receive(do: (_ -> :ok)) end)
+
+ trace(pid, [:receive], fn ->
+ send(pid, {:hello, :world})
+ assert_receive {:trace, ^pid, :receive, {:hello, :world}}
+ end)
+ end
+
+ test "delivers send trace messages to the calling process" do
+ parent = self()
+ pid = spawn_link(fn -> receive(do: (:go -> send(parent, :done))) end)
+
+ trace(pid, [:send], fn ->
+ send(pid, :go)
+ assert_receive {:trace, ^pid, :send, :done, ^parent}
+ end)
+ end
+
+ test "traces function calls" do
+ pid = spawn_link(fn -> receive(do: (:go -> Map.get(%{a: 1}, :a))) end)
+
+ trace(pid, [call: {Map, :get, 2}], fn ->
+ send(pid, :go)
+ assert_receive {:trace, ^pid, :call, {Map, :get, [%{a: 1}, :a]}}
+ end)
+ end
+
+ test "returns the value of the function" do
+ pid = spawn_link(fn -> receive(do: (_ -> :ok)) end)
+ :result = trace(pid, [:receive], fn -> :result end)
+ end
+
+ test "stops tracing and flushes messages once the function returns" do
+ pid = spawn_link(fn -> receive(do: (_ -> :ok)) end)
+
+ trace(pid, [:receive], fn -> send(pid, :during) end)
+
+ send(pid, :after)
+ refute_received {:trace, ^pid, :receive, _}
+ end
+ end
+
test "refute received does not wait" do
false = refute_received :hello
endCould be quite straightforward. Not 100% sure about function tracing. Receive is probably the most useful. We definitely need to find a good balance when to send people to just use :trace themselves. Maybe function tracing is already out of scope for the helper. |
|
@SteffenDE please send a PR for Elixir. I am not sure if we should flush the trace: we can delete messages that were not from our trace. So unless we can uniquely tag them, I wouldn't remove them. And I would keep call tracing because it can help as an alternative to mocks. Thank you! ❤️ |
|
Closing in favor of elixir-lang/elixir#15432. |
Uses OTP 27's trace sessions to trace messages received by the LiveView.
cc @josevalim
Since this is not really LiveView specific, maybe better-suited in ExUnit?