defmodule Examples.EEventBroker do @moduledoc false alias EventBroker.Event alias EventBroker.Filters alias EventBroker.Registry alias EventBroker.Supervisor use ExUnit.Case @doc """ I start the broker-registry duo. If no Broker or Registry were started beforehand, I return the message with their PIDs. If some was already started, return `:already_started`. Otherwise I error. I assert that error was not encountered and return the value. """ @spec start_broker() :: :already_started | :error | {:ok, %{broker: pid(), registry: pid()}} def start_broker do on_start = with {:ok, sup_pid} <- Supervisor.start_link() do {:ok, sup_pid} else {:error, {:already_started, _pid}} -> :already_started _ -> :error end assert on_start != :error on_start end @doc """ I am the trivial filter structure. See `EventBroker.Filters.Trivial` """ @spec trivial_filter_spec() :: EventBroker.Filters.Trivial.t() def trivial_filter_spec do %EventBroker.Filters.Trivial{} end @doc """ I am the filter structure corresponding to a filter which sorts messages depending on whether they come from the module which calls the function. See `EventBroker.Filters.SourceModule` """ @spec this_module_filter_spec() :: EventBroker.Filters.SourceModule.t() def this_module_filter_spec do %EventBroker.Filters.SourceModule{module: __MODULE__} end @doc """ I am an example message. Given a string, I return the event message for the Broker-specified format, where the source module is given by the module which sent the message and with body the specified string. """ @spec example_message_a(String.t()) :: Event.t() @spec example_message_a() :: Event.t() def example_message_a(string \\ "example body") do %Event{ source_module: __MODULE__, body: string } end @doc """ I am an example message whose source module is `Bad.Module`. """ @spec example_message_b(String.t()) :: Event.t() @spec example_message_b() :: Event.t() def example_message_b(string \\ "example body") do %Event{ source_module: Bad.Module, body: string } end @doc """ I am a function which test complex subscription. I subscribe to a chain: trivial_filter -> module_check_filer -> trivial_filter I match the event received, unsubscribe from the filter, then return the received event. """ @spec subscribe_and_check() :: {:received, Event.t()} def subscribe_and_check do EventBroker.subscribe_me([ trivial_filter_spec(), this_module_filter_spec(), trivial_filter_spec() ]) EventBroker.event(example_message_a()) EventBroker.event(example_message_b()) {:ok, event} = receive do event = %Event{} -> {:ok, event} _ -> :error end EventBroker.unsubscribe_me([ trivial_filter_spec(), this_module_filter_spec(), trivial_filter_spec() ]) {:received, event} end @doc """ I am a function which sends a million messages through a specified number of filters. Given a number `n`, I spawn an n-long chain of module-checking filters subscribed to each other, send a million messages to the Broker, then wait to see how long it takes to finish receiving them all. I then unsubscribe and return the time. """ @spec million_messages(integer()) :: {integer(), any()} def million_messages(num_filters) do filter_spec_list = for _ <- 1..num_filters do this_module_filter_spec() end EventBroker.subscribe_me(filter_spec_list) f = fn -> for _ <- 1..1_000_000 do EventBroker.event(example_message_a()) {:ok, _} = receive do event = %Event{} -> {:ok, event} _ -> :error end end :success end result = :timer.tc(f) EventBroker.unsubscribe_me(filter_spec_list) result end @doc """ I am a function unsubbing from all filters. Assuming that only one non-filter actor is subscribed to the list of the filter tree, I start the broker, then see all the filters. If they have self as a subscriber, I ask the Registry to unsubscribe. Per the hypothesis, only the Broker should remain alive after that. I check that this is indeed true and return the Registry state. """ @spec unsub_all() :: Registry.t() def unsub_all() do start_broker() # unsubscribe from all my current subscriptions EventBroker.my_subscriptions() |> Enum.each(&EventBroker.unsubscribe_me(&1)) assert EventBroker.my_subscriptions() == [] :sys.get_state(Registry) end @doc """ I check that basic subscribing works. Given a filter spec, I first unsub from everything using `unsub_all/1` then ask the Registry to subscribe me to the said filter as specified. I then check through Registry that the filter has indeed been added, that it is alive, and that its only subscriber is self. I return a tuple with the Registry state and the agent. """ @spec check_self_sub(list()) :: {Registry.t(), pid()} @spec check_self_sub() :: {Registry.t(), pid()} def check_self_sub(list \\ []) do unsub_all() assert :ok = EventBroker.subscribe_me(list) agent = :sys.get_state(Registry).registered_filters |> Map.get(list) assert Process.alive?(agent) assert self() in :sys.get_state(agent).subscribers {:sys.get_state(Registry), agent} end @doc """ I am a function checking subscription mechanism without unsubbing. I act similar to `check_self_sub/1` only I do not unsub from all the filters upon start. I just start the broker and sub to the filter. I check that it has been registered, that it is alive, and that self is one of the subscribers. """ @spec check_sub_no_unsub(list()) :: {Registry.t(), pid()} @spec check_sub_no_unsub() :: {Registry.t(), pid()} def check_sub_no_unsub(list \\ []) do start_broker() EventBroker.subscribe_me(list) agent = :sys.get_state(Registry).registered_filters |> Map.get(list) assert Process.alive?(agent) assert MapSet.new([self()]) |> MapSet.subset?(:sys.get_state(agent).subscribers) {:sys.get_state(Registry), agent} end @doc """ I check that the trivial message sort works. I start the trivial filter using `check_self_sub/1` and then send an event message using the specified string. I then assert that the message was indeed received and return the Registry state, the specified filter, as well as the PID of said filter. """ @spec message_works_trivial(String.t()) :: {Registry.t(), list(), pid()} @spec message_works_trivial() :: {Registry.t(), list(), pid()} def message_works_trivial(string \\ "subscribing works") do trivial = [trivial_filter_spec()] {_, trivial_agent} = check_self_sub(trivial) message = string |> example_message_a() EventBroker.event(message) assert_receive ^message {:sys.get_state(Registry), trivial, trivial_agent} end @doc """ I atomically check that unsubscription works. Given a filter spec, I ask to subscribe to it using `check_self_sub/1` to then ask the Registry to unsubscribe self from it. Afterwards, I check that the originally registered PID is dead and the process is deregistered. I then return the Registry state. """ @spec un_subscribing_works_atomic(list()) :: Registry.t() @spec un_subscribing_works_atomic() :: Registry.t() def un_subscribing_works_atomic(list \\ [trivial_filter_spec()]) do {state, pid} = check_self_sub(list) EventBroker.unsubscribe_me(list) refute Process.alive?(pid) refute Map.get(state, list) :sys.get_state(Registry) end @doc """ I check that the filter blocking messages based on source works. I use `check_self_sub/1` to subscribe to the filter which filters based on whether the event source was the module I am called from. Then I send the message with source from a different module and refute that it was ever received. I return a tuple with the Registry state, the filter spec, and its PID. """ @spec message_gets_blocked(String.t()) :: {Registry.t(), [Filters.SourceModule.t()], pid()} @spec message_gets_blocked() :: {Registry.t(), [Filters.SourceModule.t()], pid()} def message_gets_blocked(string \\ "blocked message") do module_spec = [this_module_filter_spec()] {_, pid} = check_self_sub(module_spec) message = string |> example_message_b() EventBroker.event(message) refute_receive ^message {:sys.get_state(Registry), module_spec, pid} end @doc """ I check that the filters get added on top of one another if some part of its dependency chain has already been registered. Given two lists of filter specs, I start the one gitten by appending both specs by first using `check_self_sub/1` on the first input and then the `check_sub_no_usub/1` on the second. I then check that the the head of the second input spec was indeed subscribed to the last element of the first input along with ourselves and that the PIDs match on initial filter startup and the final registry for the first input. """ @spec add_filter_on_top(list(), list()) :: {Registry.t(), list(), pid()} @spec add_filter_on_top(list()) :: {Registry.t(), list(), pid()} @spec add_filter_on_top() :: {Registry.t(), list(), pid()} def add_filter_on_top( filter1 \\ [trivial_filter_spec()], filter2 \\ [trivial_filter_spec()] ) do filter = filter1 ++ filter2 {_, pid1} = check_self_sub(filter1) {state, pid2} = check_sub_no_unsub(filter) registered = state.registered_filters pid_top_sub = Map.get(registered, filter1 ++ [hd(filter2)]) get_agent = Map.get(registered, filter1) assert get_agent == pid1 assert MapSet.new([self(), pid_top_sub]) == :sys.get_state(get_agent).subscribers {:sys.get_state(Registry), filter, pid2} end @doc """ I check that a complex filter chain works for filtering messages. I fist use `add_filter_on_top/2` to start a dependency chain: trivial_filter -> module_filter -> trivial_filter Afterwards, I unsubscribe from the top trivial filter, and send two messages. One is sent from self, so it should pass all filters. The other is sent from another module, so it should not get received in the end. I assert that I receive the first and refute that I receive the second one and return the Registry state. """ @spec complex_filter_message(String.t()) :: Registry.t() @spec complex_filter_message() :: Registry.t() def complex_filter_message(string \\ "complex filter message") do trivial = [trivial_filter_spec()] add_filter_on_top(trivial, [ this_module_filter_spec(), trivial_filter_spec() ]) good_msg = (string <> " good") |> example_message_a() bad_msg = (string <> " bad") |> example_message_b() EventBroker.unsubscribe_me(trivial) EventBroker.event(good_msg) EventBroker.event(bad_msg) assert_receive ^good_msg refute_receive ^bad_msg :sys.get_state(Registry) end @doc """ I check that modules with no filters cannot be registered. I unsub from all filters using `unsub_all/0` and then try to subscribe self to the filter specified by the Nock module. As the Nock module has no filters, it should return an error message. I assert that the correct error message got returned. """ @spec non_filters_fail() :: true def non_filters_fail() do unsub_all() assert "[Nock] do not export filtering functions" == EventBroker.subscribe_me([%{__struct__: :"Elixir.Nock"}]) end @doc """ I am a function that tests the unsubscription mechanism when a subscriber goes offline. I create two processes to subscribe to a chain, and then kill them one by one. I check that the filters remain in the registry after the first subscriber is killed, and that they are removed after the second subscriber is killed. """ @spec kill_subscriber() :: boolean() def kill_subscriber do # assert that the registry is now empty assert [[]] == :sys.get_state(EventBroker.Registry).registered_filters |> Map.keys() # get the pid of the shell to wait for processes to be ready this = self() # spawn a first subscriber first = spawn(fn -> Process.register(self(), :first) :ok = EventBroker.subscribe_me([ trivial_filter_spec() ]) send(this, :first) rcv = fn rcv -> receive do %Event{} -> :ok end rcv.(rcv) end rcv.(rcv) end) second = spawn(fn -> Process.register(self(), :second) :ok = EventBroker.subscribe_me([ trivial_filter_spec() ]) send(this, :second) rcv = fn rcv -> receive do %Event{} -> :ok end rcv.(rcv) end rcv.(rcv) end) receive do :first -> :ok end receive do :second -> :ok end assert [first, second] == Map.keys( :sys.get_state(EventBroker.Registry).registered_subscribers ) # kill the first subscriber Process.exit(first, :kill) Process.sleep(100) assert [[], [%EventBroker.Filters.Trivial{}]] == Map.keys(:sys.get_state(EventBroker.Registry).registered_filters) # kill the first subscriber Process.exit(second, :kill) Process.sleep(100) assert [[]] == Map.keys(:sys.get_state(EventBroker.Registry).registered_filters) end end