diff --git a/mix.exs b/mix.exs index 80f34b12d..1eceea28b 100644 --- a/mix.exs +++ b/mix.exs @@ -16,7 +16,10 @@ defmodule LiveDebugger.MixProject do name: "LiveDebugger", source_url: "https://github.com/software-mansion/live-debugger", description: "Tool for debugging LiveView applications", - docs: docs() + docs: docs(), + test_coverage: [ + ignore_modules: [~r/^LiveDebuggerDev\./, DevWeb] + ] ] end diff --git a/test/gen_servers/callback_tracing_server_test.exs b/test/gen_servers/callback_tracing_server_test.exs new file mode 100644 index 000000000..04b532fa9 --- /dev/null +++ b/test/gen_servers/callback_tracing_server_test.exs @@ -0,0 +1,195 @@ +defmodule LiveDebugger.GenServers.CallbackTracingServerTest do + @moduledoc false + use ExUnit.Case, async: true + + import Mox + + alias LiveDebugger.Structs.Trace + alias LiveDebugger.GenServers.CallbackTracingServer + alias LiveDebugger.Utils.PubSub, as: PubSubUtils + alias LiveDebugger.MockModuleService + alias LiveDebugger.MockDbg + alias LiveDebugger.MockEtsTableServer + alias LiveDebugger.MockPubSubUtils + alias LiveDebugger.MockProcessService + + @modules [ + CoolApp.LiveViews.UserDashboard, + CoolApp.Service.UserService, + CoolApp.LiveComponent.UserElement + ] + + setup :verify_on_exit! + + test "init/1" do + assert {:ok, %{}} = CallbackTracingServer.init([]) + assert_receive :setup_tracing + end + + test "handle_call/3" do + assert {:reply, :ok, %{}} == CallbackTracingServer.handle_call(:ping, self(), %{}) + end + + test "proper tracing setup" do + MockModuleService + |> expect(:all, fn -> + Enum.map(@modules, fn module -> {to_charlist(module), ~c"", false} end) + end) + |> expect(:loaded?, 6, fn _module -> true end) + |> expect(:behaviours, 6, fn module -> get_behaviours(module) end) + + MockDbg + |> expect(:tracer, fn :process, {_handler, 0} -> :ok end) + |> expect(:p, fn :all, :c -> :ok end) + + get_live_view_callbacks(CoolApp.LiveViews.UserDashboard) + |> Enum.each(&expect(MockDbg, :tp, fn &1, [] -> :ok end)) + + get_live_component_callbacks(CoolApp.LiveComponent.UserElement) + |> Enum.each(&expect(MockDbg, :tp, fn &1, [] -> :ok end)) + + MockDbg + |> expect(:tp, fn {Phoenix.LiveView.Diff, :delete_component, 2}, [] -> :ok end) + + assert {:noreply, %{}} = CallbackTracingServer.handle_info(:setup_tracing, %{}) + end + + describe "tracing mechanism" do + setup do + parent = self() + + MockModuleService + |> expect(:all, fn -> [] end) + + MockDbg + |> expect(:p, fn :all, :c -> :ok end) + |> expect(:tp, fn {Phoenix.LiveView.Diff, :delete_component, 2}, [] -> :ok end) + |> expect(:tracer, fn :process, {handle_trace, 0} -> send(parent, handle_trace) end) + + :ok + end + + test "handle delete component trace" do + parent = self() + transport_pid = :c.pid(0, 0, 1) + pid = :c.pid(0, 0, 2) + cid = 3 + socket_id = "phx-GDrDzLLr4USWzwBC" + module = Phoenix.LiveView.Diff + function = :delete_component + args = [cid, %{}] + + expected_topic = + PubSubUtils.component_deleted_topic(%{socket_id: socket_id, transport_pid: transport_pid}) + + MockProcessService + |> expect(:state, fn ^pid -> + {:ok, LiveDebugger.Fakes.state(transport_pid: transport_pid, socket_id: socket_id)} + end) + + MockPubSubUtils + |> expect(:broadcast, fn ^expected_topic, {:new_trace, trace} -> + send(parent, {:trace, trace}) + end) + + assert {:noreply, %{}} = CallbackTracingServer.handle_info(:setup_tracing, %{}) + + assert_receive handle_trace + assert 0 == handle_trace.({:trace, pid, :call, {module, function, args}}, 0) + assert_receive {:trace, trace} + + assert %Trace{ + id: 0, + module: ^module, + function: ^function, + arity: 2, + args: ^args, + socket_id: ^socket_id, + transport_pid: ^transport_pid, + pid: ^pid, + cid: %Phoenix.LiveComponent.CID{cid: ^cid} + } = trace + end + + test "handle standard live view trace" do + transport_pid = :c.pid(0, 0, 1) + pid = :c.pid(0, 0, 2) + socket_id = "phx-GDrDzLLr4USWzwBC" + module = CoolApp.LiveViews.UserDashboard + function = :handle_info + + args = [ + :msg, + %{transport_pid: transport_pid, socket: %Phoenix.LiveView.Socket{id: socket_id}} + ] + + table = :ets.new(:test_table, [:ordered_set, :public]) + + expected_tsnf_topic = PubSubUtils.tsnf_topic(socket_id, transport_pid, pid, function) + expected_ts_f_topic = PubSubUtils.ts_f_topic(socket_id, transport_pid, function) + + MockEtsTableServer + |> expect(:table!, fn ^pid -> table end) + + MockPubSubUtils + |> expect(:broadcast, fn ^expected_tsnf_topic, {:new_trace, _trace} -> :ok end) + |> expect(:broadcast, fn ^expected_ts_f_topic, {:new_trace, _trace} -> :ok end) + + assert {:noreply, %{}} = CallbackTracingServer.handle_info(:setup_tracing, %{}) + assert_receive handle_trace + assert -1 == handle_trace.({:trace, pid, :call, {module, function, args}}, 0) + assert [{0, trace}] = :ets.tab2list(table) + + assert %Trace{ + id: 0, + module: ^module, + function: ^function, + arity: 2, + args: ^args, + socket_id: ^socket_id, + transport_pid: ^transport_pid, + pid: ^pid, + cid: nil + } = trace + end + + test "handle unexpected trace" do + assert {:noreply, %{}} = CallbackTracingServer.handle_info(:setup_tracing, %{}) + assert_receive handle_trace + assert 0 == handle_trace.({:_, :c.pid(0, 0, 1), :_, {SomeModule, :some_func, []}}, 0) + end + end + + defp get_behaviours(module) do + case module do + CoolApp.LiveViews.UserDashboard -> [Phoenix.LiveView] + CoolApp.Service.UserService -> [] + CoolApp.LiveComponent.UserElement -> [Phoenix.LiveComponent] + end + end + + defp get_live_view_callbacks(module) do + [ + {module, :mount, 3}, + {module, :handle_params, 3}, + {module, :handle_info, 2}, + {module, :handle_call, 3}, + {module, :handle_cast, 2}, + {module, :terminate, 2}, + {module, :render, 1}, + {module, :handle_event, 3}, + {module, :handle_async, 3} + ] + end + + defp get_live_component_callbacks(module) do + [ + {module, :mount, 1}, + {module, :update, 2}, + {module, :update_many, 1}, + {module, :render, 1}, + {module, :handle_event, 3}, + {module, :handle_async, 3} + ] + end +end diff --git a/test/gen_servers/ets_table_server_test.exs b/test/gen_servers/ets_table_server_test.exs index 7b00800d6..768af3411 100644 --- a/test/gen_servers/ets_table_server_test.exs +++ b/test/gen_servers/ets_table_server_test.exs @@ -2,9 +2,13 @@ defmodule LiveDebugger.GenServers.EtsTableServerTest do @moduledoc false use ExUnit.Case, async: true + import Mox + alias LiveDebugger.Utils.PubSub, as: PubSubUtils alias LiveDebugger.GenServers.EtsTableServer + setup :verify_on_exit! + test "start_link/1" do assert {:ok, _pid} = EtsTableServer.start_link() GenServer.stop(EtsTableServer) @@ -19,7 +23,7 @@ defmodule LiveDebugger.GenServers.EtsTableServerTest do pid = :c.pid(0, 0, 1) LiveDebugger.MockEtsTableServer - |> Mox.expect(:table!, fn ^pid -> :some_ref end) + |> expect(:table!, fn ^pid -> :some_ref end) assert :some_ref = EtsTableServer.table!(pid) end @@ -28,7 +32,7 @@ defmodule LiveDebugger.GenServers.EtsTableServerTest do pid = :c.pid(0, 0, 1) LiveDebugger.MockEtsTableServer - |> Mox.expect(:delete_table!, fn ^pid -> :ok end) + |> expect(:delete_table!, fn ^pid -> :ok end) assert :ok = EtsTableServer.delete_table!(pid) end @@ -47,7 +51,7 @@ defmodule LiveDebugger.GenServers.EtsTableServerTest do topic = PubSubUtils.process_status_topic(pid) LiveDebugger.MockPubSubUtils - |> Mox.expect(:broadcast, fn ^topic, {:process_status, :dead} -> :ok end) + |> expect(:broadcast, fn ^topic, {:process_status, :dead} -> :ok end) assert {:noreply, new_table_refs} = EtsTableServer.handle_info({:DOWN, :_, :process, pid, :_}, table_refs) diff --git a/test/services/trace_service_test.exs b/test/services/trace_service_test.exs new file mode 100644 index 000000000..f189533e6 --- /dev/null +++ b/test/services/trace_service_test.exs @@ -0,0 +1,155 @@ +defmodule Services.TraceServiceTest do + use ExUnit.Case, async: true + + import Mox + + alias LiveDebugger.Structs.Trace + alias LiveDebugger.Services.TraceService + alias LiveDebugger.MockEtsTableServer + + setup :verify_on_exit! + + setup_all do + %{ + module: CoolApp.LiveViews.UserDashboard, + pid: :c.pid(0, 0, 1) + } + end + + setup context do + table = :ets.new(:trace_table, [:ordered_set, :public]) + + Map.put(context, :table, table) + end + + test "insert/1", %{module: module, pid: pid, table: table} do + trace = Trace.new(1, module, :render, [], pid) + + MockEtsTableServer + |> expect(:table!, fn ^pid -> table end) + + assert true == TraceService.insert(trace) + assert [{trace.id, trace}] == :ets.lookup(table, trace.id) + end + + test "get/2", %{module: module, pid: pid, table: table} do + trace1 = Trace.new(1, module, :handle_info, [], pid) + trace2 = Trace.new(2, module, :render, [], pid) + :ets.insert(table, {trace1.id, trace1}) + :ets.insert(table, {trace2.id, trace2}) + + MockEtsTableServer + |> expect(:table!, 3, fn ^pid -> table end) + + assert trace1 == TraceService.get(pid, trace1.id) + assert trace2 == TraceService.get(pid, trace2.id) + assert nil == TraceService.get(pid, 99) + end + + describe "existing_traces/2" do + test "returns traces with default limit", %{module: module, pid: pid, table: table} do + trace1 = Trace.new(1, module, :handle_info, [], pid) + trace2 = Trace.new(2, module, :render, [], pid) + :ets.insert(table, {trace1.id, trace1}) + :ets.insert(table, {trace2.id, trace2}) + + MockEtsTableServer + |> expect(:table!, fn ^pid -> table end) + + assert {[^trace1, ^trace2], _} = TraceService.existing_traces(pid) + end + + test "returns traces with limit and continuation", %{module: module, pid: pid, table: table} do + trace1 = Trace.new(1, module, :handle_info, [], pid) + trace2 = Trace.new(2, module, :render, [], pid) + trace3 = Trace.new(3, module, :handle_event, [], pid) + :ets.insert(table, {trace1.id, trace1}) + :ets.insert(table, {trace2.id, trace2}) + :ets.insert(table, {trace3.id, trace3}) + + MockEtsTableServer + |> expect(:table!, fn ^pid -> table end) + + {traces1, cont} = TraceService.existing_traces(pid, limit: 2) + {traces2, cont} = TraceService.existing_traces(pid, cont: cont) + + assert [trace1, trace2] == traces1 + assert [trace3] == traces2 + assert cont == :end_of_table + assert :end_of_table == TraceService.existing_traces(pid, cont: :end_of_table) + end + + test "raise ArgumentError when limit is less than 1", %{pid: pid} do + assert_raise ArgumentError, fn -> TraceService.existing_traces(pid, limit: 0) end + assert_raise ArgumentError, fn -> TraceService.existing_traces(pid, limit: -23) end + end + + test "returns traces with functions filter", %{module: module, pid: pid, table: table} do + trace1 = Trace.new(1, module, :handle_info, [], pid) + trace2 = Trace.new(2, module, :render, [], pid) + trace3 = Trace.new(3, module, :handle_event, [], pid) + :ets.insert(table, {trace1.id, trace1}) + :ets.insert(table, {trace2.id, trace2}) + :ets.insert(table, {trace3.id, trace3}) + + MockEtsTableServer + |> expect(:table!, 2, fn ^pid -> table end) + + assert {[^trace1], _} = TraceService.existing_traces(pid, functions: [:handle_info]) + + assert {[^trace1, ^trace2], _} = + TraceService.existing_traces(pid, functions: [:handle_info, :render, :mount]) + end + + test "returns traces with node_id filter", %{module: module, pid: pid, table: table} do + cid = %Phoenix.LiveComponent.CID{cid: 3} + trace1 = Trace.new(1, module, :handle_info, [], pid) + trace2 = Trace.new(2, module, :render, [], pid, cid: cid) + trace3 = Trace.new(3, module, :handle_event, [], pid, cid: cid) + :ets.insert(table, {trace1.id, trace1}) + :ets.insert(table, {trace2.id, trace2}) + :ets.insert(table, {trace3.id, trace3}) + + MockEtsTableServer + |> expect(:table!, 2, fn ^pid -> table end) + + assert {[^trace1], _} = TraceService.existing_traces(pid, node_id: pid) + assert {[^trace2, ^trace3], _} = TraceService.existing_traces(pid, node_id: cid) + end + + test "returns :end_of_table when no traces match", %{module: module, pid: pid, table: table} do + trace1 = Trace.new(1, module, :handle_info, [], pid) + trace2 = Trace.new(2, module, :render, [], pid) + :ets.insert(table, {trace1.id, trace1}) + :ets.insert(table, {trace2.id, trace2}) + + MockEtsTableServer + |> expect(:table!, fn ^pid -> table end) + + assert :end_of_table = TraceService.existing_traces(pid, functions: [:non_existent]) + end + end + + describe "clear_traces/2" do + test "clears traces for LiveView or LiveComponent", %{module: module, pid: pid, table: table} do + cid = %Phoenix.LiveComponent.CID{cid: 3} + trace1 = Trace.new(1, module, :handle_info, [], pid) + trace2 = Trace.new(2, module, :render, [], pid, cid: cid) + :ets.insert(table, {trace1.id, trace1}) + :ets.insert(table, {trace2.id, trace2}) + + MockEtsTableServer + |> expect(:table!, 5, fn ^pid -> table end) + + assert {[^trace1, ^trace2], _} = TraceService.existing_traces(pid) + + TraceService.clear_traces(pid, trace1.pid) + + assert {[^trace2], _} = TraceService.existing_traces(pid) + + TraceService.clear_traces(pid, trace2.cid) + + assert :end_of_table = TraceService.existing_traces(pid) + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 9faf5ea83..d713bffd6 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -17,6 +17,9 @@ else Mox.defmock(LiveDebugger.MockEtsTableServer, for: LiveDebugger.GenServers.EtsTableServer) Application.put_env(:live_debugger, :ets_table_server, LiveDebugger.MockEtsTableServer) + + Mox.defmock(LiveDebugger.MockDbg, for: LiveDebugger.Services.System.DbgService) + Application.put_env(:live_debugger, :dbg_service, LiveDebugger.MockDbg) end ExUnit.start()