|
| 1 | +defmodule Sentry.Test do |
| 2 | + @moduledoc """ |
| 3 | + Utilities for testing Sentry reports. |
| 4 | +
|
| 5 | + ## Usage |
| 6 | +
|
| 7 | + This module is based on **collecting** reported events and then retrieving |
| 8 | + them to perform assertions. You can start collecting events from a process |
| 9 | + by calling `start_collecting_sentry_reports/0`. Then, you can use Sentry |
| 10 | + as normal and report events (through functions such as `Sentry.capture_message/1` |
| 11 | + or `Sentry.capture_exception/1`). Finally, you can retrieve the collected events |
| 12 | + by calling `pop_sentry_reports/0`. |
| 13 | +
|
| 14 | + ## Examples |
| 15 | +
|
| 16 | + Let's imagine writing a test using the functions in this module. First, we need to |
| 17 | + start collecting events: |
| 18 | +
|
| 19 | + test "reporting from child processes" do |
| 20 | + parent_pid = self() |
| 21 | +
|
| 22 | + # Collect reports from self(). |
| 23 | + assert :ok = Test.start_collecting_sentry_reports() |
| 24 | +
|
| 25 | + # <we'll fill this in below...> |
| 26 | + end |
| 27 | +
|
| 28 | + Now, we can report events as normal. For example, we can report an event from the |
| 29 | + parent process: |
| 30 | +
|
| 31 | + assert {:ok, ""} = Sentry.capture_message("Oops from parent process") |
| 32 | +
|
| 33 | + We can also report events from "child" processes. |
| 34 | +
|
| 35 | + # Spawn a child that waits for the :go message and then reports an event. |
| 36 | + {:ok, child_pid} = |
| 37 | + Task.start_link(fn -> |
| 38 | + receive do |
| 39 | + :go -> |
| 40 | + assert {:ok, ""} = Sentry.capture_message("Oops from child process") |
| 41 | + send(parent_pid, :done) |
| 42 | + end |
| 43 | + end) |
| 44 | +
|
| 45 | + # Start the child and wait for it to finish. |
| 46 | + send(child_pid, :go) |
| 47 | + assert_receive :done |
| 48 | +
|
| 49 | + Now, we can retrieve the collected events and perform assertions on them: |
| 50 | +
|
| 51 | + assert [%Event{} = event1, %Event{} = event2] = Test.pop_sentry_reports() |
| 52 | + assert event1.message.formatted == "Oops from parent process" |
| 53 | + assert event2.message.formatted == "Oops from child process" |
| 54 | +
|
| 55 | + """ |
| 56 | + |
| 57 | + @moduledoc since: "10.2.0" |
| 58 | + |
| 59 | + @server __MODULE__.OwnershipServer |
| 60 | + @key :events |
| 61 | + |
| 62 | + # Used internally when reporting an event, *before* reporting the actual event. |
| 63 | + @doc false |
| 64 | + @spec maybe_collect(Sentry.Event.t()) :: :collected | :not_collecting |
| 65 | + def maybe_collect(%Sentry.Event{} = event) do |
| 66 | + if Sentry.Config.test_mode?() do |
| 67 | + case NimbleOwnership.fetch_owner(@server, callers(), @key) do |
| 68 | + {:ok, owner_pid} -> |
| 69 | + result = |
| 70 | + NimbleOwnership.get_and_update(@server, owner_pid, @key, fn events -> |
| 71 | + {:collected, (events || []) ++ [event]} |
| 72 | + end) |
| 73 | + |
| 74 | + case result do |
| 75 | + {:ok, :collected} -> |
| 76 | + :collected |
| 77 | + |
| 78 | + {:error, error} -> |
| 79 | + raise ArgumentError, "cannot collect Sentry reports: #{Exception.message(error)}" |
| 80 | + end |
| 81 | + |
| 82 | + :error -> |
| 83 | + :not_collecting |
| 84 | + end |
| 85 | + else |
| 86 | + :not_collecting |
| 87 | + end |
| 88 | + end |
| 89 | + |
| 90 | + @doc """ |
| 91 | + Starts collecting events from the current process. |
| 92 | +
|
| 93 | + This function starts collecting events reported from the current process. If you want to |
| 94 | + allow other processes to report events, you need to *allow* them to report events back |
| 95 | + to the current process. See `allow/2` for more information on allowances. If the current |
| 96 | + process is already *allowed by another process*, this function raises an error. |
| 97 | +
|
| 98 | + The `context` parameter is ignored. It's there so that this function can be used |
| 99 | + as an ExUnit **setup callback**. For example: |
| 100 | +
|
| 101 | + import Sentry.Test |
| 102 | +
|
| 103 | + setup :start_collecting_sentry_reports |
| 104 | +
|
| 105 | + """ |
| 106 | + @doc since: "10.2.0" |
| 107 | + @spec start_collecting_sentry_reports(map()) :: :ok |
| 108 | + def start_collecting_sentry_reports(_context \\ %{}) do |
| 109 | + # Make sure the ownership server is started (this is idempotent). |
| 110 | + ensure_ownership_server_started() |
| 111 | + |
| 112 | + case NimbleOwnership.fetch_owner(@server, callers(), @key) do |
| 113 | + # No-op |
| 114 | + {tag, owner_pid} when tag in [:ok, :shared_owner] and owner_pid == self() -> |
| 115 | + :ok |
| 116 | + |
| 117 | + {:shared_owner, _pid} -> |
| 118 | + raise ArgumentError, |
| 119 | + "Sentry.Test is in global mode and is already collecting reported events" |
| 120 | + |
| 121 | + {:ok, another_pid} -> |
| 122 | + raise ArgumentError, "already collecting reported events from #{inspect(another_pid)}" |
| 123 | + |
| 124 | + :error -> |
| 125 | + :ok |
| 126 | + end |
| 127 | + |
| 128 | + {:ok, _} = |
| 129 | + NimbleOwnership.get_and_update(@server, self(), @key, fn events -> |
| 130 | + {:ignored, events || []} |
| 131 | + end) |
| 132 | + |
| 133 | + :ok |
| 134 | + end |
| 135 | + |
| 136 | + @doc """ |
| 137 | + Allows `pid_to_allow` to collect events back to the root process via `owner_pid`. |
| 138 | +
|
| 139 | + `owner_pid` must be a PID that is currently collecting events or has been allowed |
| 140 | + to collect events. If that's not the case, this function raises an error. |
| 141 | +
|
| 142 | + `pid_to_allow` can also be a **function** that returns a PID. This is useful when |
| 143 | + you want to allow a registered process that is not yet started to collect events. For example: |
| 144 | +
|
| 145 | + Sentry.Test.allow_sentry_reports(self(), fn -> Process.whereis(:my_process) end) |
| 146 | +
|
| 147 | + """ |
| 148 | + @doc since: "10.2.0" |
| 149 | + @spec allow_sentry_reports(pid(), pid() | (-> pid())) :: :ok |
| 150 | + def allow_sentry_reports(owner_pid, pid_to_allow) |
| 151 | + when is_pid(owner_pid) and (is_pid(pid_to_allow) or is_function(pid_to_allow, 0)) do |
| 152 | + case NimbleOwnership.allow(@server, owner_pid, pid_to_allow, @key) do |
| 153 | + :ok -> |
| 154 | + :ok |
| 155 | + |
| 156 | + {:error, reason} -> |
| 157 | + raise "failed to allow #{inspect(pid_to_allow)} to collect events: #{Exception.message(reason)}" |
| 158 | + end |
| 159 | + end |
| 160 | + |
| 161 | + @doc """ |
| 162 | + Pops all the collected events from the current process. |
| 163 | +
|
| 164 | + This function returns a list of all the events that have been collected from the current |
| 165 | + process and all the processes that were allowed through it. If the current process |
| 166 | + is not collecting events, this function raises an error. |
| 167 | +
|
| 168 | + After this function returns, the current process will still be collecting events, but |
| 169 | + the collected events will be reset to `[]`. |
| 170 | +
|
| 171 | + ## Examples |
| 172 | +
|
| 173 | + iex> Sentry.Test.start_collecting_sentry_reports() |
| 174 | + :ok |
| 175 | + iex> Sentry.capture_message("Oops") |
| 176 | + {:ok, ""} |
| 177 | + iex> [%Sentry.Event{} = event] = Sentry.Test.pop_sentry_reports() |
| 178 | + iex> event.message.formatted |
| 179 | + "Oops" |
| 180 | +
|
| 181 | + """ |
| 182 | + @doc since: "10.2.0" |
| 183 | + @spec pop_sentry_reports() :: [Sentry.Event.t()] |
| 184 | + def pop_sentry_reports do |
| 185 | + result = |
| 186 | + NimbleOwnership.get_and_update(@server, self(), @key, fn |
| 187 | + nil -> {:not_collecting, []} |
| 188 | + events when is_list(events) -> {events, []} |
| 189 | + end) |
| 190 | + |
| 191 | + case result do |
| 192 | + {:ok, :not_collecting} -> |
| 193 | + raise ArgumentError, "not collecting reported events from #{inspect(self())}" |
| 194 | + |
| 195 | + {:ok, events} -> |
| 196 | + events |
| 197 | + |
| 198 | + {:error, error} when is_exception(error) -> |
| 199 | + raise ArgumentError, "cannot pop Sentry reports: #{Exception.message(error)}" |
| 200 | + end |
| 201 | + end |
| 202 | + |
| 203 | + ## Helpers |
| 204 | + |
| 205 | + defp ensure_ownership_server_started do |
| 206 | + case NimbleOwnership.start_link(name: @server) do |
| 207 | + {:ok, pid} -> |
| 208 | + pid |
| 209 | + |
| 210 | + {:error, {:already_started, pid}} -> |
| 211 | + pid |
| 212 | + |
| 213 | + {:error, reason} -> |
| 214 | + raise "could not start required processes for Sentry.Test: #{inspect(reason)}" |
| 215 | + end |
| 216 | + end |
| 217 | + |
| 218 | + defp callers do |
| 219 | + [self()] ++ Process.get(:"$callers", []) |
| 220 | + end |
| 221 | +end |
0 commit comments