From b665ddd6db823bf24ec039087fdda8367ba27b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 29 May 2024 15:44:16 +0200 Subject: [PATCH] Add parameterized tests (#13618) Sometimes you want to run the same tests but with different parameters. In ExUnit, it is possible to do so by passing a `:parameterize` key to `ExUnit.Case`. The value must be a list of maps which will be the parameters merged into the test context. For example, Elixir has a module called `Registry`, which can have type `:unique` or `:duplicate`, and can control its concurrency factor using the `:partitions` option. If you have a number of tests that *behave the same* across all of those values, you can parameterize those tests with: use ExUnit.Case, async: true, parameterize: for(kind <- [:unique, :duplicate], partitions <- [1, 8], do: %{kind: kind, partitions: partitions}) Then, in your tests, you can access the parameters as part of the context: test "starts a registry", %{kind: kind, partitions: partitions} do ... end Use parameterized tests with care: * Although parameterized tests run concurrently when `async: true` is also given, abuse of parameterized tests may make your test suite slower * If you use parameterized tests and then find yourself adding conditionals in your tests to deal with different parameters, then parameterized tests may be the wrong solution to your problem. Consider creating separated tests and sharing logic between them using regular functions --- lib/elixir/test/elixir/registry_test.exs | 1514 +++++++++---------- lib/ex_unit/lib/ex_unit.ex | 17 +- lib/ex_unit/lib/ex_unit/case.ex | 89 +- lib/ex_unit/lib/ex_unit/cli_formatter.ex | 7 +- lib/ex_unit/lib/ex_unit/formatter.ex | 20 +- lib/ex_unit/lib/ex_unit/runner.ex | 46 +- lib/ex_unit/lib/ex_unit/server.ex | 24 +- lib/ex_unit/test/ex_unit/case_test.exs | 22 +- lib/ex_unit/test/ex_unit/formatter_test.exs | 25 + lib/ex_unit/test/ex_unit_test.exs | 26 +- 10 files changed, 929 insertions(+), 861 deletions(-) diff --git a/lib/elixir/test/elixir/registry_test.exs b/lib/elixir/test/elixir/registry_test.exs index 68d099467dd..e2dc21a5b54 100644 --- a/lib/elixir/test/elixir/registry_test.exs +++ b/lib/elixir/test/elixir/registry_test.exs @@ -1,959 +1,959 @@ Code.require_file("test_helper.exs", __DIR__) -defmodule RegistryTest do +defmodule Registry.CommonTest do use ExUnit.Case, async: true doctest Registry, except: [:moduledoc] +end + +defmodule Registry.Test do + use ExUnit.Case, + async: true, + parameterize: [ + %{partitions: 1}, + %{partitions: 8} + ] setup config do - keys = config[:keys] || :unique - partitions = config[:partitions] || 1 - listeners = List.wrap(config[:listener]) - opts = [keys: keys, name: config.test, partitions: partitions, listeners: listeners] + keys = config.keys || :unique + partitions = config.partitions + listeners = List.wrap(config[:base_listener]) |> Enum.map(&:"#{&1}_#{partitions}") + name = :"#{config.test}_#{partitions}" + opts = [keys: keys, name: name, partitions: partitions, listeners: listeners] {:ok, _} = start_supervised({Registry, opts}) - {:ok, %{registry: config.test, partitions: partitions}} + %{registry: name, listeners: listeners} end - for {describe, partitions} <- ["with 1 partition": 1, "with 8 partitions": 8] do - describe "unique #{describe}" do - @describetag keys: :unique, partitions: partitions + describe "unique" do + @describetag keys: :unique - test "starts configured number of partitions", %{registry: registry, partitions: partitions} do - assert length(Supervisor.which_children(registry)) == partitions - end + test "starts configured number of partitions", %{registry: registry, partitions: partitions} do + assert length(Supervisor.which_children(registry)) == partitions + end - test "counts 0 keys in an empty registry", %{registry: registry} do - assert 0 == Registry.count(registry) - end + test "counts 0 keys in an empty registry", %{registry: registry} do + assert 0 == Registry.count(registry) + end - test "counts the number of keys in a registry", %{registry: registry} do - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "world", :value) + test "counts the number of keys in a registry", %{registry: registry} do + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "world", :value) - assert 2 == Registry.count(registry) - end + assert 2 == Registry.count(registry) + end - test "has unique registrations", %{registry: registry} do - {:ok, pid} = Registry.register(registry, "hello", :value) - assert is_pid(pid) - assert Registry.keys(registry, self()) == ["hello"] - assert Registry.values(registry, "hello", self()) == [:value] + test "has unique registrations", %{registry: registry} do + {:ok, pid} = Registry.register(registry, "hello", :value) + assert is_pid(pid) + assert Registry.keys(registry, self()) == ["hello"] + assert Registry.values(registry, "hello", self()) == [:value] - assert {:error, {:already_registered, pid}} = Registry.register(registry, "hello", :value) - assert pid == self() - assert Registry.keys(registry, self()) == ["hello"] - assert Registry.values(registry, "hello", self()) == [:value] + assert {:error, {:already_registered, pid}} = Registry.register(registry, "hello", :value) + assert pid == self() + assert Registry.keys(registry, self()) == ["hello"] + assert Registry.values(registry, "hello", self()) == [:value] - {:ok, pid} = Registry.register(registry, "world", :value) - assert is_pid(pid) - assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "world"] - end + {:ok, pid} = Registry.register(registry, "world", :value) + assert is_pid(pid) + assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "world"] + end - test "has unique registrations across processes", %{registry: registry} do - {_, task} = register_task(registry, "hello", :value) - Process.link(Process.whereis(registry)) - assert Registry.keys(registry, task) == ["hello"] - assert Registry.values(registry, "hello", task) == [:value] + test "has unique registrations across processes", %{registry: registry} do + {_, task} = register_task(registry, "hello", :value) + Process.link(Process.whereis(registry)) + assert Registry.keys(registry, task) == ["hello"] + assert Registry.values(registry, "hello", task) == [:value] - assert {:error, {:already_registered, ^task}} = - Registry.register(registry, "hello", :recent) + assert {:error, {:already_registered, ^task}} = + Registry.register(registry, "hello", :recent) - assert Registry.keys(registry, self()) == [] - assert Registry.values(registry, "hello", self()) == [] + assert Registry.keys(registry, self()) == [] + assert Registry.values(registry, "hello", self()) == [] - {:links, links} = Process.info(self(), :links) - assert Process.whereis(registry) in links - end + {:links, links} = Process.info(self(), :links) + assert Process.whereis(registry) in links + end - test "has unique registrations even if partition is delayed", %{registry: registry} do - {owner, task} = register_task(registry, "hello", :value) + test "has unique registrations even if partition is delayed", %{registry: registry} do + {owner, task} = register_task(registry, "hello", :value) - assert Registry.register(registry, "hello", :other) == - {:error, {:already_registered, task}} + assert Registry.register(registry, "hello", :other) == + {:error, {:already_registered, task}} - :sys.suspend(owner) - kill_and_assert_down(task) - Registry.register(registry, "hello", :other) - assert Registry.lookup(registry, "hello") == [{self(), :other}] - end + :sys.suspend(owner) + kill_and_assert_down(task) + Registry.register(registry, "hello", :other) + assert Registry.lookup(registry, "hello") == [{self(), :other}] + end - test "supports match patterns", %{registry: registry} do - value = {1, :atom, 1} - {:ok, _} = Registry.register(registry, "hello", value) - assert Registry.match(registry, "hello", {1, :_, :_}) == [{self(), value}] - assert Registry.match(registry, "hello", {1.0, :_, :_}) == [] - assert Registry.match(registry, "hello", {:_, :atom, :_}) == [{self(), value}] - assert Registry.match(registry, "hello", {:"$1", :_, :"$1"}) == [{self(), value}] - assert Registry.match(registry, "hello", :_) == [{self(), value}] - assert Registry.match(registry, :_, :_) == [] - - value2 = %{a: "a", b: "b"} - {:ok, _} = Registry.register(registry, "world", value2) - assert Registry.match(registry, "world", %{b: "b"}) == [{self(), value2}] - end + test "supports match patterns", %{registry: registry} do + value = {1, :atom, 1} + {:ok, _} = Registry.register(registry, "hello", value) + assert Registry.match(registry, "hello", {1, :_, :_}) == [{self(), value}] + assert Registry.match(registry, "hello", {1.0, :_, :_}) == [] + assert Registry.match(registry, "hello", {:_, :atom, :_}) == [{self(), value}] + assert Registry.match(registry, "hello", {:"$1", :_, :"$1"}) == [{self(), value}] + assert Registry.match(registry, "hello", :_) == [{self(), value}] + assert Registry.match(registry, :_, :_) == [] + + value2 = %{a: "a", b: "b"} + {:ok, _} = Registry.register(registry, "world", value2) + assert Registry.match(registry, "world", %{b: "b"}) == [{self(), value2}] + end - test "supports guard conditions", %{registry: registry} do - value = {1, :atom, 2} - {:ok, _} = Registry.register(registry, "hello", value) + test "supports guard conditions", %{registry: registry} do + value = {1, :atom, 2} + {:ok, _} = Registry.register(registry, "hello", value) - assert Registry.match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}]) == - [{self(), value}] + assert Registry.match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}]) == + [{self(), value}] - assert Registry.match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 2}]) == [] + assert Registry.match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 2}]) == [] - assert Registry.match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) == - [{self(), value}] - end + assert Registry.match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) == + [{self(), value}] + end - test "count_match supports match patterns", %{registry: registry} do - value = {1, :atom, 1} - {:ok, _} = Registry.register(registry, "hello", value) - assert 1 == Registry.count_match(registry, "hello", {1, :_, :_}) - assert 0 == Registry.count_match(registry, "hello", {1.0, :_, :_}) - assert 1 == Registry.count_match(registry, "hello", {:_, :atom, :_}) - assert 1 == Registry.count_match(registry, "hello", {:"$1", :_, :"$1"}) - assert 1 == Registry.count_match(registry, "hello", :_) - assert 0 == Registry.count_match(registry, :_, :_) - - value2 = %{a: "a", b: "b"} - {:ok, _} = Registry.register(registry, "world", value2) - assert 1 == Registry.count_match(registry, "world", %{b: "b"}) - end + test "count_match supports match patterns", %{registry: registry} do + value = {1, :atom, 1} + {:ok, _} = Registry.register(registry, "hello", value) + assert 1 == Registry.count_match(registry, "hello", {1, :_, :_}) + assert 0 == Registry.count_match(registry, "hello", {1.0, :_, :_}) + assert 1 == Registry.count_match(registry, "hello", {:_, :atom, :_}) + assert 1 == Registry.count_match(registry, "hello", {:"$1", :_, :"$1"}) + assert 1 == Registry.count_match(registry, "hello", :_) + assert 0 == Registry.count_match(registry, :_, :_) + + value2 = %{a: "a", b: "b"} + {:ok, _} = Registry.register(registry, "world", value2) + assert 1 == Registry.count_match(registry, "world", %{b: "b"}) + end - test "count_match supports guard conditions", %{registry: registry} do - value = {1, :atom, 2} - {:ok, _} = Registry.register(registry, "hello", value) + test "count_match supports guard conditions", %{registry: registry} do + value = {1, :atom, 2} + {:ok, _} = Registry.register(registry, "hello", value) - assert 1 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}]) - assert 0 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 2}]) - assert 1 == Registry.count_match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) - end + assert 1 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}]) + assert 0 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 2}]) + assert 1 == Registry.count_match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) + end - test "unregister_match supports patterns", %{registry: registry} do - value = {1, :atom, 1} - {:ok, _} = Registry.register(registry, "hello", value) + test "unregister_match supports patterns", %{registry: registry} do + value = {1, :atom, 1} + {:ok, _} = Registry.register(registry, "hello", value) - Registry.unregister_match(registry, "hello", {2, :_, :_}) - assert Registry.lookup(registry, "hello") == [{self(), value}] - Registry.unregister_match(registry, "hello", {1.0, :_, :_}) - assert Registry.lookup(registry, "hello") == [{self(), value}] - Registry.unregister_match(registry, "hello", {:_, :atom, :_}) - assert Registry.lookup(registry, "hello") == [] - end + Registry.unregister_match(registry, "hello", {2, :_, :_}) + assert Registry.lookup(registry, "hello") == [{self(), value}] + Registry.unregister_match(registry, "hello", {1.0, :_, :_}) + assert Registry.lookup(registry, "hello") == [{self(), value}] + Registry.unregister_match(registry, "hello", {:_, :atom, :_}) + assert Registry.lookup(registry, "hello") == [] + end - test "unregister_match supports guards", %{registry: registry} do - value = {1, :atom, 1} - {:ok, _} = Registry.register(registry, "hello", value) + test "unregister_match supports guards", %{registry: registry} do + value = {1, :atom, 1} + {:ok, _} = Registry.register(registry, "hello", value) - Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}]) - assert Registry.lookup(registry, "hello") == [] - end + Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}]) + assert Registry.lookup(registry, "hello") == [] + end - test "unregister_match supports tricky keys", %{registry: registry} do - {:ok, _} = Registry.register(registry, :_, :foo) - {:ok, _} = Registry.register(registry, "hello", "b") + test "unregister_match supports tricky keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, :foo) + {:ok, _} = Registry.register(registry, "hello", "b") - Registry.unregister_match(registry, :_, :foo) - assert Registry.lookup(registry, :_) == [] - assert Registry.keys(registry, self()) |> Enum.sort() == ["hello"] - end + Registry.unregister_match(registry, :_, :foo) + assert Registry.lookup(registry, :_) == [] + assert Registry.keys(registry, self()) |> Enum.sort() == ["hello"] + end - test "compares using ===", %{registry: registry} do - {:ok, _} = Registry.register(registry, 1.0, :value) - {:ok, _} = Registry.register(registry, 1, :value) - assert Registry.keys(registry, self()) |> Enum.sort() == [1, 1.0] - end + test "compares using ===", %{registry: registry} do + {:ok, _} = Registry.register(registry, 1.0, :value) + {:ok, _} = Registry.register(registry, 1, :value) + assert Registry.keys(registry, self()) |> Enum.sort() == [1, 1.0] + end - test "updates current process value", %{registry: registry} do - assert Registry.update_value(registry, "hello", &raise/1) == :error - register_task(registry, "hello", :value) - assert Registry.update_value(registry, "hello", &raise/1) == :error + test "updates current process value", %{registry: registry} do + assert Registry.update_value(registry, "hello", &raise/1) == :error + register_task(registry, "hello", :value) + assert Registry.update_value(registry, "hello", &raise/1) == :error - Registry.register(registry, "world", 1) - assert Registry.lookup(registry, "world") == [{self(), 1}] - assert Registry.update_value(registry, "world", &(&1 + 1)) == {2, 1} - assert Registry.lookup(registry, "world") == [{self(), 2}] - end + Registry.register(registry, "world", 1) + assert Registry.lookup(registry, "world") == [{self(), 1}] + assert Registry.update_value(registry, "world", &(&1 + 1)) == {2, 1} + assert Registry.lookup(registry, "world") == [{self(), 2}] + end - test "dispatches to a single key", %{registry: registry} do - fun = fn _ -> raise "will never be invoked" end - assert Registry.dispatch(registry, "hello", fun) == :ok + test "dispatches to a single key", %{registry: registry} do + fun = fn _ -> raise "will never be invoked" end + assert Registry.dispatch(registry, "hello", fun) == :ok - {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "hello", :value) - fun = fn [{pid, value}] -> send(pid, {:dispatch, value}) end - assert Registry.dispatch(registry, "hello", fun) + fun = fn [{pid, value}] -> send(pid, {:dispatch, value}) end + assert Registry.dispatch(registry, "hello", fun) - assert_received {:dispatch, :value} - end + assert_received {:dispatch, :value} + end - test "unregisters process by key", %{registry: registry} do - :ok = Registry.unregister(registry, "hello") + test "unregisters process by key", %{registry: registry} do + :ok = Registry.unregister(registry, "hello") - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "world", :value) - assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "world"] + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "world", :value) + assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "world"] - :ok = Registry.unregister(registry, "hello") - assert Registry.keys(registry, self()) == ["world"] + :ok = Registry.unregister(registry, "hello") + assert Registry.keys(registry, self()) == ["world"] - :ok = Registry.unregister(registry, "world") - assert Registry.keys(registry, self()) == [] - end + :ok = Registry.unregister(registry, "world") + assert Registry.keys(registry, self()) == [] + end - test "unregisters with no entries", %{registry: registry} do - assert Registry.unregister(registry, "hello") == :ok - end + test "unregisters with no entries", %{registry: registry} do + assert Registry.unregister(registry, "hello") == :ok + end - test "unregisters with tricky keys", %{registry: registry} do - {:ok, _} = Registry.register(registry, :_, :foo) - {:ok, _} = Registry.register(registry, "hello", "b") + test "unregisters with tricky keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, :foo) + {:ok, _} = Registry.register(registry, "hello", "b") - Registry.unregister(registry, :_) - assert Registry.lookup(registry, :_) == [] - assert Registry.keys(registry, self()) |> Enum.sort() == ["hello"] - end + Registry.unregister(registry, :_) + assert Registry.lookup(registry, :_) == [] + assert Registry.keys(registry, self()) |> Enum.sort() == ["hello"] + end - @tag listener: :"unique_listener_#{partitions}" - test "allows listeners", %{registry: registry, listener: listener} do - Process.register(self(), listener) - {_, task} = register_task(registry, "hello", :world) - assert_received {:register, ^registry, "hello", ^task, :world} + @tag base_listener: :unique_listener + test "allows listeners", %{registry: registry, listeners: [listener]} do + Process.register(self(), listener) + {_, task} = register_task(registry, "hello", :world) + assert_received {:register, ^registry, "hello", ^task, :world} - self = self() - {:ok, _} = Registry.register(registry, "world", :value) - assert_received {:register, ^registry, "world", ^self, :value} + self = self() + {:ok, _} = Registry.register(registry, "world", :value) + assert_received {:register, ^registry, "world", ^self, :value} - :ok = Registry.unregister(registry, "world") - assert_received {:unregister, ^registry, "world", ^self} - end + :ok = Registry.unregister(registry, "world") + assert_received {:unregister, ^registry, "world", ^self} + after + Process.unregister(listener) + end - test "links and unlinks on register/unregister", %{registry: registry} do - {:ok, pid} = Registry.register(registry, "hello", :value) - {:links, links} = Process.info(self(), :links) - assert pid in links + test "links and unlinks on register/unregister", %{registry: registry} do + {:ok, pid} = Registry.register(registry, "hello", :value) + {:links, links} = Process.info(self(), :links) + assert pid in links - {:ok, pid} = Registry.register(registry, "world", :value) - {:links, links} = Process.info(self(), :links) - assert pid in links + {:ok, pid} = Registry.register(registry, "world", :value) + {:links, links} = Process.info(self(), :links) + assert pid in links - :ok = Registry.unregister(registry, "hello") - {:links, links} = Process.info(self(), :links) - assert pid in links + :ok = Registry.unregister(registry, "hello") + {:links, links} = Process.info(self(), :links) + assert pid in links - :ok = Registry.unregister(registry, "world") - {:links, links} = Process.info(self(), :links) - refute pid in links - end + :ok = Registry.unregister(registry, "world") + {:links, links} = Process.info(self(), :links) + refute pid in links + end - test "raises on unknown registry name" do - assert_raise ArgumentError, ~r/unknown registry/, fn -> - Registry.register(:unknown, "hello", :value) - end + test "raises on unknown registry name" do + assert_raise ArgumentError, ~r/unknown registry/, fn -> + Registry.register(:unknown, "hello", :value) end + end - test "via callbacks", %{registry: registry} do - name = {:via, Registry, {registry, "hello"}} + test "via callbacks", %{registry: registry} do + name = {:via, Registry, {registry, "hello"}} - # register_name - {:ok, pid} = Agent.start_link(fn -> 0 end, name: name) + # register_name + {:ok, pid} = Agent.start_link(fn -> 0 end, name: name) - # send - assert Agent.update(name, &(&1 + 1)) == :ok + # send + assert Agent.update(name, &(&1 + 1)) == :ok - # whereis_name - assert Agent.get(name, & &1) == 1 + # whereis_name + assert Agent.get(name, & &1) == 1 - # unregister_name - assert {:error, _} = Agent.start(fn -> raise "oops" end) + # unregister_name + assert {:error, _} = Agent.start(fn -> raise "oops" end) - # errors - assert {:error, {:already_started, ^pid}} = Agent.start(fn -> 0 end, name: name) - end + # errors + assert {:error, {:already_started, ^pid}} = Agent.start(fn -> 0 end, name: name) + end - test "uses value provided in via", %{registry: registry} do - name = {:via, Registry, {registry, "hello", :value}} - {:ok, pid} = Agent.start_link(fn -> 0 end, name: name) - assert Registry.lookup(registry, "hello") == [{pid, :value}] - end + test "uses value provided in via", %{registry: registry} do + name = {:via, Registry, {registry, "hello", :value}} + {:ok, pid} = Agent.start_link(fn -> 0 end, name: name) + assert Registry.lookup(registry, "hello") == [{pid, :value}] + end - test "empty list for empty registry", %{registry: registry} do - assert Registry.select(registry, [{{:_, :_, :_}, [], [:"$_"]}]) == [] - end + test "empty list for empty registry", %{registry: registry} do + assert Registry.select(registry, [{{:_, :_, :_}, [], [:"$_"]}]) == [] + end - test "select all", %{registry: registry} do - name = {:via, Registry, {registry, "hello"}} - {:ok, pid} = Agent.start_link(fn -> 0 end, name: name) - {:ok, _} = Registry.register(registry, "world", :value) + test "select all", %{registry: registry} do + name = {:via, Registry, {registry, "hello"}} + {:ok, pid} = Agent.start_link(fn -> 0 end, name: name) + {:ok, _} = Registry.register(registry, "world", :value) - assert Registry.select(registry, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) - |> Enum.sort() == - [{"hello", pid, nil}, {"world", self(), :value}] - end + assert Registry.select(registry, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) + |> Enum.sort() == + [{"hello", pid, nil}, {"world", self(), :value}] + end - test "select supports full match specs", %{registry: registry} do - value = {1, :atom, 1} - {:ok, _} = Registry.register(registry, "hello", value) + test "select supports full match specs", %{registry: registry} do + value = {1, :atom, 1} + {:ok, _} = Registry.register(registry, "hello", value) - assert [{"hello", self(), value}] == - Registry.select(registry, [ - {{"hello", :"$2", :"$3"}, [], [{{"hello", :"$2", :"$3"}}]} - ]) + assert [{"hello", self(), value}] == + Registry.select(registry, [ + {{"hello", :"$2", :"$3"}, [], [{{"hello", :"$2", :"$3"}}]} + ]) - assert [{"hello", self(), value}] == - Registry.select(registry, [ - {{:"$1", self(), :"$3"}, [], [{{:"$1", self(), :"$3"}}]} - ]) + assert [{"hello", self(), value}] == + Registry.select(registry, [ + {{:"$1", self(), :"$3"}, [], [{{:"$1", self(), :"$3"}}]} + ]) - assert [{"hello", self(), value}] == - Registry.select(registry, [ - {{:"$1", :"$2", value}, [], [{{:"$1", :"$2", {value}}}]} - ]) + assert [{"hello", self(), value}] == + Registry.select(registry, [ + {{:"$1", :"$2", value}, [], [{{:"$1", :"$2", {value}}}]} + ]) - assert [] == - Registry.select(registry, [ - {{"world", :"$2", :"$3"}, [], [{{"world", :"$2", :"$3"}}]} - ]) + assert [] == + Registry.select(registry, [ + {{"world", :"$2", :"$3"}, [], [{{"world", :"$2", :"$3"}}]} + ]) - assert [] == Registry.select(registry, [{{:"$1", :"$2", {1.0, :_, :_}}, [], [:"$_"]}]) + assert [] == Registry.select(registry, [{{:"$1", :"$2", {1.0, :_, :_}}, [], [:"$_"]}]) - assert [{"hello", self(), value}] == - Registry.select(registry, [ - {{:"$1", :"$2", {:"$3", :atom, :"$4"}}, [], - [{{:"$1", :"$2", {{:"$3", :atom, :"$4"}}}}]} - ]) + assert [{"hello", self(), value}] == + Registry.select(registry, [ + {{:"$1", :"$2", {:"$3", :atom, :"$4"}}, [], + [{{:"$1", :"$2", {{:"$3", :atom, :"$4"}}}}]} + ]) - assert [{"hello", self(), {1, :atom, 1}}] == - Registry.select(registry, [ - {{:"$1", :"$2", {:"$3", :"$4", :"$3"}}, [], - [{{:"$1", :"$2", {{:"$3", :"$4", :"$3"}}}}]} - ]) + assert [{"hello", self(), {1, :atom, 1}}] == + Registry.select(registry, [ + {{:"$1", :"$2", {:"$3", :"$4", :"$3"}}, [], + [{{:"$1", :"$2", {{:"$3", :"$4", :"$3"}}}}]} + ]) - value2 = %{a: "a", b: "b"} - {:ok, _} = Registry.register(registry, "world", value2) + value2 = %{a: "a", b: "b"} + {:ok, _} = Registry.register(registry, "world", value2) - assert [:match] == - Registry.select(registry, [{{"world", self(), %{b: "b"}}, [], [:match]}]) + assert [:match] == + Registry.select(registry, [{{"world", self(), %{b: "b"}}, [], [:match]}]) - assert ["hello", "world"] == - Registry.select(registry, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort() - end + assert ["hello", "world"] == + Registry.select(registry, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort() + end - test "select supports guard conditions", %{registry: registry} do - value = {1, :atom, 2} - {:ok, _} = Registry.register(registry, "hello", value) - - assert [{"hello", self(), {1, :atom, 2}}] == - Registry.select(registry, [ - {{:"$1", :"$2", {:"$3", :"$4", :"$5"}}, [{:>, :"$5", 1}], - [{{:"$1", :"$2", {{:"$3", :"$4", :"$5"}}}}]} - ]) - - assert [] == - Registry.select(registry, [ - {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [:"$_"]} - ]) - - assert ["hello"] == - Registry.select(registry, [ - {{:"$1", :_, {:_, :"$2", :_}}, [{:is_atom, :"$2"}], [:"$1"]} - ]) - end + test "select supports guard conditions", %{registry: registry} do + value = {1, :atom, 2} + {:ok, _} = Registry.register(registry, "hello", value) + + assert [{"hello", self(), {1, :atom, 2}}] == + Registry.select(registry, [ + {{:"$1", :"$2", {:"$3", :"$4", :"$5"}}, [{:>, :"$5", 1}], + [{{:"$1", :"$2", {{:"$3", :"$4", :"$5"}}}}]} + ]) + + assert [] == + Registry.select(registry, [ + {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [:"$_"]} + ]) + + assert ["hello"] == + Registry.select(registry, [ + {{:"$1", :_, {:_, :"$2", :_}}, [{:is_atom, :"$2"}], [:"$1"]} + ]) + end - test "select allows multiple specs", %{registry: registry} do - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "world", :value) + test "select allows multiple specs", %{registry: registry} do + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "world", :value) - assert ["hello", "world"] == - Registry.select(registry, [ - {{"hello", :_, :_}, [], [{:element, 1, :"$_"}]}, - {{"world", :_, :_}, [], [{:element, 1, :"$_"}]} - ]) - |> Enum.sort() - end + assert ["hello", "world"] == + Registry.select(registry, [ + {{"hello", :_, :_}, [], [{:element, 1, :"$_"}]}, + {{"world", :_, :_}, [], [{:element, 1, :"$_"}]} + ]) + |> Enum.sort() + end - test "select raises on incorrect shape of match spec", %{registry: registry} do - assert_raise ArgumentError, fn -> - Registry.select(registry, [{:_, [], []}]) - end + test "select raises on incorrect shape of match spec", %{registry: registry} do + assert_raise ArgumentError, fn -> + Registry.select(registry, [{:_, [], []}]) end + end - test "count_select supports match specs", %{registry: registry} do - value = {1, :atom, 1} - {:ok, _} = Registry.register(registry, "hello", value) - assert 1 == Registry.count_select(registry, [{{:_, :_, value}, [], [true]}]) - assert 1 == Registry.count_select(registry, [{{"hello", :_, :_}, [], [true]}]) - assert 1 == Registry.count_select(registry, [{{:_, :_, {1, :atom, :_}}, [], [true]}]) - assert 1 == Registry.count_select(registry, [{{:_, :_, {:"$1", :_, :"$1"}}, [], [true]}]) - assert 0 == Registry.count_select(registry, [{{"hello", :_, nil}, [], [true]}]) - - value2 = %{a: "a", b: "b"} - {:ok, _} = Registry.register(registry, "world", value2) - assert 1 == Registry.count_select(registry, [{{"world", :_, :_}, [], [true]}]) - end + test "count_select supports match specs", %{registry: registry} do + value = {1, :atom, 1} + {:ok, _} = Registry.register(registry, "hello", value) + assert 1 == Registry.count_select(registry, [{{:_, :_, value}, [], [true]}]) + assert 1 == Registry.count_select(registry, [{{"hello", :_, :_}, [], [true]}]) + assert 1 == Registry.count_select(registry, [{{:_, :_, {1, :atom, :_}}, [], [true]}]) + assert 1 == Registry.count_select(registry, [{{:_, :_, {:"$1", :_, :"$1"}}, [], [true]}]) + assert 0 == Registry.count_select(registry, [{{"hello", :_, nil}, [], [true]}]) + + value2 = %{a: "a", b: "b"} + {:ok, _} = Registry.register(registry, "world", value2) + assert 1 == Registry.count_select(registry, [{{"world", :_, :_}, [], [true]}]) + end - test "count_select supports guard conditions", %{registry: registry} do - value = {1, :atom, 2} - {:ok, _} = Registry.register(registry, "hello", value) + test "count_select supports guard conditions", %{registry: registry} do + value = {1, :atom, 2} + {:ok, _} = Registry.register(registry, "hello", value) - assert 1 == - Registry.count_select(registry, [ - {{:_, :_, {:_, :"$1", :_}}, [{:is_atom, :"$1"}], [true]} - ]) + assert 1 == + Registry.count_select(registry, [ + {{:_, :_, {:_, :"$1", :_}}, [{:is_atom, :"$1"}], [true]} + ]) - assert 1 == - Registry.count_select(registry, [ - {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 1}], [true]} - ]) + assert 1 == + Registry.count_select(registry, [ + {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 1}], [true]} + ]) - assert 0 == - Registry.count_select(registry, [ - {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [true]} - ]) - end + assert 0 == + Registry.count_select(registry, [ + {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [true]} + ]) + end - test "count_select allows multiple specs", %{registry: registry} do - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "world", :value) + test "count_select allows multiple specs", %{registry: registry} do + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "world", :value) - assert 2 == - Registry.count_select(registry, [ - {{"hello", :_, :_}, [], [true]}, - {{"world", :_, :_}, [], [true]} - ]) - end + assert 2 == + Registry.count_select(registry, [ + {{"hello", :_, :_}, [], [true]}, + {{"world", :_, :_}, [], [true]} + ]) + end - test "count_select raises on incorrect shape of match spec", %{registry: registry} do - assert_raise ArgumentError, fn -> - Registry.count_select(registry, [{:_, [], []}]) - end + test "count_select raises on incorrect shape of match spec", %{registry: registry} do + assert_raise ArgumentError, fn -> + Registry.count_select(registry, [{:_, [], []}]) end + end - test "doesn't grow ets on already_registered", - %{registry: registry, partitions: partitions} do - assert sum_pid_entries(registry, partitions) == 0 + test "doesn't grow ets on already_registered", + %{registry: registry, partitions: partitions} do + assert sum_pid_entries(registry, partitions) == 0 - {:ok, pid} = Registry.register(registry, "hello", :value) - assert is_pid(pid) - assert sum_pid_entries(registry, partitions) == 1 + {:ok, pid} = Registry.register(registry, "hello", :value) + assert is_pid(pid) + assert sum_pid_entries(registry, partitions) == 1 - {:ok, pid} = Registry.register(registry, "world", :value) - assert is_pid(pid) - assert sum_pid_entries(registry, partitions) == 2 + {:ok, pid} = Registry.register(registry, "world", :value) + assert is_pid(pid) + assert sum_pid_entries(registry, partitions) == 2 - assert {:error, {:already_registered, _pid}} = - Registry.register(registry, "hello", :value) + assert {:error, {:already_registered, _pid}} = + Registry.register(registry, "hello", :value) - assert sum_pid_entries(registry, partitions) == 2 - end + assert sum_pid_entries(registry, partitions) == 2 + end - test "doesn't grow ets on already_registered across processes", - %{registry: registry, partitions: partitions} do - assert sum_pid_entries(registry, partitions) == 0 + test "doesn't grow ets on already_registered across processes", + %{registry: registry, partitions: partitions} do + assert sum_pid_entries(registry, partitions) == 0 - {_, task} = register_task(registry, "hello", :value) - Process.link(Process.whereis(registry)) + {_, task} = register_task(registry, "hello", :value) + Process.link(Process.whereis(registry)) - assert sum_pid_entries(registry, partitions) == 1 + assert sum_pid_entries(registry, partitions) == 1 - {:ok, pid} = Registry.register(registry, "world", :value) - assert is_pid(pid) - assert sum_pid_entries(registry, partitions) == 2 + {:ok, pid} = Registry.register(registry, "world", :value) + assert is_pid(pid) + assert sum_pid_entries(registry, partitions) == 2 - assert {:error, {:already_registered, ^task}} = - Registry.register(registry, "hello", :recent) + assert {:error, {:already_registered, ^task}} = + Registry.register(registry, "hello", :recent) - assert sum_pid_entries(registry, partitions) == 2 - end + assert sum_pid_entries(registry, partitions) == 2 end end - for {describe, partitions} <- ["with 1 partition": 1, "with 8 partitions": 8] do - describe "duplicate #{describe}" do - @describetag keys: :duplicate, partitions: partitions + describe "duplicate" do + @describetag keys: :duplicate - test "starts configured number of partitions", %{registry: registry, partitions: partitions} do - assert length(Supervisor.which_children(registry)) == partitions - end - - test "counts 0 keys in an empty registry", %{registry: registry} do - assert 0 == Registry.count(registry) - end + test "starts configured number of partitions", %{registry: registry, partitions: partitions} do + assert length(Supervisor.which_children(registry)) == partitions + end - test "counts the number of keys in a registry", %{registry: registry} do - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "hello", :value) + test "counts 0 keys in an empty registry", %{registry: registry} do + assert 0 == Registry.count(registry) + end - assert 2 == Registry.count(registry) - end + test "counts the number of keys in a registry", %{registry: registry} do + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "hello", :value) - test "has duplicate registrations", %{registry: registry} do - {:ok, pid} = Registry.register(registry, "hello", :value) - assert is_pid(pid) - assert Registry.keys(registry, self()) == ["hello"] - assert Registry.values(registry, "hello", self()) == [:value] + assert 2 == Registry.count(registry) + end - assert {:ok, pid} = Registry.register(registry, "hello", :value) - assert is_pid(pid) - assert Registry.keys(registry, self()) == ["hello", "hello"] - assert Registry.values(registry, "hello", self()) == [:value, :value] + test "has duplicate registrations", %{registry: registry} do + {:ok, pid} = Registry.register(registry, "hello", :value) + assert is_pid(pid) + assert Registry.keys(registry, self()) == ["hello"] + assert Registry.values(registry, "hello", self()) == [:value] - {:ok, pid} = Registry.register(registry, "world", :value) - assert is_pid(pid) - assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello", "world"] - end + assert {:ok, pid} = Registry.register(registry, "hello", :value) + assert is_pid(pid) + assert Registry.keys(registry, self()) == ["hello", "hello"] + assert Registry.values(registry, "hello", self()) == [:value, :value] - test "has duplicate registrations across processes", %{registry: registry} do - {_, task} = register_task(registry, "hello", :world) - assert Registry.keys(registry, self()) == [] - assert Registry.keys(registry, task) == ["hello"] - assert Registry.values(registry, "hello", self()) == [] - assert Registry.values(registry, "hello", task) == [:world] + {:ok, pid} = Registry.register(registry, "world", :value) + assert is_pid(pid) + assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello", "world"] + end - assert {:ok, _pid} = Registry.register(registry, "hello", :value) - assert Registry.keys(registry, self()) == ["hello"] - assert Registry.values(registry, "hello", self()) == [:value] - end + test "has duplicate registrations across processes", %{registry: registry} do + {_, task} = register_task(registry, "hello", :world) + assert Registry.keys(registry, self()) == [] + assert Registry.keys(registry, task) == ["hello"] + assert Registry.values(registry, "hello", self()) == [] + assert Registry.values(registry, "hello", task) == [:world] - test "compares using matches", %{registry: registry} do - {:ok, _} = Registry.register(registry, 1.0, :value) - {:ok, _} = Registry.register(registry, 1, :value) - assert Registry.keys(registry, self()) |> Enum.sort() == [1, 1.0] - end + assert {:ok, _pid} = Registry.register(registry, "hello", :value) + assert Registry.keys(registry, self()) == ["hello"] + assert Registry.values(registry, "hello", self()) == [:value] + end - test "dispatches to multiple keys in serial", %{registry: registry} do - Process.flag(:trap_exit, true) - parent = self() + test "compares using matches", %{registry: registry} do + {:ok, _} = Registry.register(registry, 1.0, :value) + {:ok, _} = Registry.register(registry, 1, :value) + assert Registry.keys(registry, self()) |> Enum.sort() == [1, 1.0] + end - fun = fn _ -> raise "will never be invoked" end - assert Registry.dispatch(registry, "hello", fun, parallel: false) == :ok + test "dispatches to multiple keys in serial", %{registry: registry} do + Process.flag(:trap_exit, true) + parent = self() - {:ok, _} = Registry.register(registry, "hello", :value1) - {:ok, _} = Registry.register(registry, "hello", :value2) - {:ok, _} = Registry.register(registry, "world", :value3) + fun = fn _ -> raise "will never be invoked" end + assert Registry.dispatch(registry, "hello", fun, parallel: false) == :ok - fun = fn entries -> - assert parent == self() - for {pid, value} <- entries, do: send(pid, {:dispatch, value}) - end + {:ok, _} = Registry.register(registry, "hello", :value1) + {:ok, _} = Registry.register(registry, "hello", :value2) + {:ok, _} = Registry.register(registry, "world", :value3) - assert Registry.dispatch(registry, "hello", fun, parallel: false) + fun = fn entries -> + assert parent == self() + for {pid, value} <- entries, do: send(pid, {:dispatch, value}) + end - assert_received {:dispatch, :value1} - assert_received {:dispatch, :value2} - refute_received {:dispatch, :value3} + assert Registry.dispatch(registry, "hello", fun, parallel: false) - fun = fn entries -> - assert parent == self() - for {pid, value} <- entries, do: send(pid, {:dispatch, value}) - end + assert_received {:dispatch, :value1} + assert_received {:dispatch, :value2} + refute_received {:dispatch, :value3} - assert Registry.dispatch(registry, "world", fun, parallel: false) + fun = fn entries -> + assert parent == self() + for {pid, value} <- entries, do: send(pid, {:dispatch, value}) + end - refute_received {:dispatch, :value1} - refute_received {:dispatch, :value2} - assert_received {:dispatch, :value3} + assert Registry.dispatch(registry, "world", fun, parallel: false) - refute_received {:EXIT, _, _} - end + refute_received {:dispatch, :value1} + refute_received {:dispatch, :value2} + assert_received {:dispatch, :value3} - test "dispatches to multiple keys in parallel", context do - %{registry: registry, partitions: partitions} = context - Process.flag(:trap_exit, true) - parent = self() + refute_received {:EXIT, _, _} + end - fun = fn _ -> raise "will never be invoked" end - assert Registry.dispatch(registry, "hello", fun, parallel: true) == :ok + test "dispatches to multiple keys in parallel", context do + %{registry: registry, partitions: partitions} = context + Process.flag(:trap_exit, true) + parent = self() - {:ok, _} = Registry.register(registry, "hello", :value1) - {:ok, _} = Registry.register(registry, "hello", :value2) - {:ok, _} = Registry.register(registry, "world", :value3) + fun = fn _ -> raise "will never be invoked" end + assert Registry.dispatch(registry, "hello", fun, parallel: true) == :ok - fun = fn entries -> - if partitions == 8 do - assert parent != self() - else - assert parent == self() - end + {:ok, _} = Registry.register(registry, "hello", :value1) + {:ok, _} = Registry.register(registry, "hello", :value2) + {:ok, _} = Registry.register(registry, "world", :value3) - for {pid, value} <- entries, do: send(pid, {:dispatch, value}) + fun = fn entries -> + if partitions == 8 do + assert parent != self() + else + assert parent == self() end - assert Registry.dispatch(registry, "hello", fun, parallel: true) + for {pid, value} <- entries, do: send(pid, {:dispatch, value}) + end - assert_received {:dispatch, :value1} - assert_received {:dispatch, :value2} - refute_received {:dispatch, :value3} + assert Registry.dispatch(registry, "hello", fun, parallel: true) - fun = fn entries -> - if partitions == 8 do - assert parent != self() - else - assert parent == self() - end + assert_received {:dispatch, :value1} + assert_received {:dispatch, :value2} + refute_received {:dispatch, :value3} - for {pid, value} <- entries, do: send(pid, {:dispatch, value}) + fun = fn entries -> + if partitions == 8 do + assert parent != self() + else + assert parent == self() end - assert Registry.dispatch(registry, "world", fun, parallel: true) + for {pid, value} <- entries, do: send(pid, {:dispatch, value}) + end - refute_received {:dispatch, :value1} - refute_received {:dispatch, :value2} - assert_received {:dispatch, :value3} + assert Registry.dispatch(registry, "world", fun, parallel: true) - refute_received {:EXIT, _, _} - end + refute_received {:dispatch, :value1} + refute_received {:dispatch, :value2} + assert_received {:dispatch, :value3} - test "unregisters by key", %{registry: registry} do - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "world", :value) - assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello", "world"] + refute_received {:EXIT, _, _} + end - :ok = Registry.unregister(registry, "hello") - assert Registry.keys(registry, self()) == ["world"] + test "unregisters by key", %{registry: registry} do + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "world", :value) + assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello", "world"] - :ok = Registry.unregister(registry, "world") - assert Registry.keys(registry, self()) == [] - end + :ok = Registry.unregister(registry, "hello") + assert Registry.keys(registry, self()) == ["world"] - test "unregisters with no entries", %{registry: registry} do - assert Registry.unregister(registry, "hello") == :ok - end + :ok = Registry.unregister(registry, "world") + assert Registry.keys(registry, self()) == [] + end - test "unregisters with tricky keys", %{registry: registry} do - {:ok, _} = Registry.register(registry, :_, :foo) - {:ok, _} = Registry.register(registry, :_, :bar) - {:ok, _} = Registry.register(registry, "hello", "a") - {:ok, _} = Registry.register(registry, "hello", "b") + test "unregisters with no entries", %{registry: registry} do + assert Registry.unregister(registry, "hello") == :ok + end - Registry.unregister(registry, :_) - assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello"] - end + test "unregisters with tricky keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, :foo) + {:ok, _} = Registry.register(registry, :_, :bar) + {:ok, _} = Registry.register(registry, "hello", "a") + {:ok, _} = Registry.register(registry, "hello", "b") - test "supports match patterns", %{registry: registry} do - value1 = {1, :atom, 1} - value2 = {2, :atom, 2} + Registry.unregister(registry, :_) + assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello"] + end - {:ok, _} = Registry.register(registry, "hello", value1) - {:ok, _} = Registry.register(registry, "hello", value2) + test "supports match patterns", %{registry: registry} do + value1 = {1, :atom, 1} + value2 = {2, :atom, 2} - assert Registry.match(registry, "hello", {1, :_, :_}) == [{self(), value1}] - assert Registry.match(registry, "hello", {1.0, :_, :_}) == [] + {:ok, _} = Registry.register(registry, "hello", value1) + {:ok, _} = Registry.register(registry, "hello", value2) - assert Registry.match(registry, "hello", {:_, :atom, :_}) |> Enum.sort() == - [{self(), value1}, {self(), value2}] + assert Registry.match(registry, "hello", {1, :_, :_}) == [{self(), value1}] + assert Registry.match(registry, "hello", {1.0, :_, :_}) == [] - assert Registry.match(registry, "hello", {:"$1", :_, :"$1"}) |> Enum.sort() == - [{self(), value1}, {self(), value2}] + assert Registry.match(registry, "hello", {:_, :atom, :_}) |> Enum.sort() == + [{self(), value1}, {self(), value2}] - assert Registry.match(registry, "hello", {2, :_, :_}) == [{self(), value2}] - assert Registry.match(registry, "hello", {2.0, :_, :_}) == [] - end + assert Registry.match(registry, "hello", {:"$1", :_, :"$1"}) |> Enum.sort() == + [{self(), value1}, {self(), value2}] - test "supports guards", %{registry: registry} do - value1 = {1, :atom, 1} - value2 = {2, :atom, 2} + assert Registry.match(registry, "hello", {2, :_, :_}) == [{self(), value2}] + assert Registry.match(registry, "hello", {2.0, :_, :_}) == [] + end - {:ok, _} = Registry.register(registry, "hello", value1) - {:ok, _} = Registry.register(registry, "hello", value2) + test "supports guards", %{registry: registry} do + value1 = {1, :atom, 1} + value2 = {2, :atom, 2} - assert Registry.match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}]) == - [{self(), value1}] + {:ok, _} = Registry.register(registry, "hello", value1) + {:ok, _} = Registry.register(registry, "hello", value2) - assert Registry.match(registry, "hello", {:"$1", :_, :_}, [{:>, :"$1", 3}]) == [] + assert Registry.match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}]) == + [{self(), value1}] - assert Registry.match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 3}]) |> Enum.sort() == - [{self(), value1}, {self(), value2}] + assert Registry.match(registry, "hello", {:"$1", :_, :_}, [{:>, :"$1", 3}]) == [] - assert Registry.match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) - |> Enum.sort() == [{self(), value1}, {self(), value2}] - end + assert Registry.match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 3}]) |> Enum.sort() == + [{self(), value1}, {self(), value2}] - test "count_match supports match patterns", %{registry: registry} do - value = {1, :atom, 1} - {:ok, _} = Registry.register(registry, "hello", value) - assert 1 == Registry.count_match(registry, "hello", {1, :_, :_}) - assert 0 == Registry.count_match(registry, "hello", {1.0, :_, :_}) - assert 1 == Registry.count_match(registry, "hello", {:_, :atom, :_}) - assert 1 == Registry.count_match(registry, "hello", {:"$1", :_, :"$1"}) - assert 1 == Registry.count_match(registry, "hello", :_) - assert 0 == Registry.count_match(registry, :_, :_) - - value2 = %{a: "a", b: "b"} - {:ok, _} = Registry.register(registry, "world", value2) - assert 1 == Registry.count_match(registry, "world", %{b: "b"}) - end + assert Registry.match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) + |> Enum.sort() == [{self(), value1}, {self(), value2}] + end - test "count_match supports guard conditions", %{registry: registry} do - value = {1, :atom, 2} - {:ok, _} = Registry.register(registry, "hello", value) + test "count_match supports match patterns", %{registry: registry} do + value = {1, :atom, 1} + {:ok, _} = Registry.register(registry, "hello", value) + assert 1 == Registry.count_match(registry, "hello", {1, :_, :_}) + assert 0 == Registry.count_match(registry, "hello", {1.0, :_, :_}) + assert 1 == Registry.count_match(registry, "hello", {:_, :atom, :_}) + assert 1 == Registry.count_match(registry, "hello", {:"$1", :_, :"$1"}) + assert 1 == Registry.count_match(registry, "hello", :_) + assert 0 == Registry.count_match(registry, :_, :_) + + value2 = %{a: "a", b: "b"} + {:ok, _} = Registry.register(registry, "world", value2) + assert 1 == Registry.count_match(registry, "world", %{b: "b"}) + end - assert 1 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}]) - assert 0 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 2}]) - assert 1 == Registry.count_match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) - end + test "count_match supports guard conditions", %{registry: registry} do + value = {1, :atom, 2} + {:ok, _} = Registry.register(registry, "hello", value) - test "unregister_match supports patterns", %{registry: registry} do - value1 = {1, :atom, 1} - value2 = {2, :atom, 2} + assert 1 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}]) + assert 0 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 2}]) + assert 1 == Registry.count_match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) + end - {:ok, _} = Registry.register(registry, "hello", value1) - {:ok, _} = Registry.register(registry, "hello", value2) + test "unregister_match supports patterns", %{registry: registry} do + value1 = {1, :atom, 1} + value2 = {2, :atom, 2} - Registry.unregister_match(registry, "hello", {2, :_, :_}) - assert Registry.lookup(registry, "hello") == [{self(), value1}] + {:ok, _} = Registry.register(registry, "hello", value1) + {:ok, _} = Registry.register(registry, "hello", value2) - {:ok, _} = Registry.register(registry, "hello", value2) - Registry.unregister_match(registry, "hello", {2.0, :_, :_}) - assert Registry.lookup(registry, "hello") == [{self(), value1}, {self(), value2}] - Registry.unregister_match(registry, "hello", {:_, :atom, :_}) - assert Registry.lookup(registry, "hello") == [] - end + Registry.unregister_match(registry, "hello", {2, :_, :_}) + assert Registry.lookup(registry, "hello") == [{self(), value1}] - test "unregister_match supports guards", %{registry: registry} do - value1 = {1, :atom, 1} - value2 = {2, :atom, 2} + {:ok, _} = Registry.register(registry, "hello", value2) + Registry.unregister_match(registry, "hello", {2.0, :_, :_}) + assert Registry.lookup(registry, "hello") == [{self(), value1}, {self(), value2}] + Registry.unregister_match(registry, "hello", {:_, :atom, :_}) + assert Registry.lookup(registry, "hello") == [] + end - {:ok, _} = Registry.register(registry, "hello", value1) - {:ok, _} = Registry.register(registry, "hello", value2) + test "unregister_match supports guards", %{registry: registry} do + value1 = {1, :atom, 1} + value2 = {2, :atom, 2} - Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}]) - assert Registry.lookup(registry, "hello") == [{self(), value2}] - end + {:ok, _} = Registry.register(registry, "hello", value1) + {:ok, _} = Registry.register(registry, "hello", value2) - test "unregister_match supports tricky keys", %{registry: registry} do - {:ok, _} = Registry.register(registry, :_, :foo) - {:ok, _} = Registry.register(registry, :_, :bar) - {:ok, _} = Registry.register(registry, "hello", "a") - {:ok, _} = Registry.register(registry, "hello", "b") + Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}]) + assert Registry.lookup(registry, "hello") == [{self(), value2}] + end - Registry.unregister_match(registry, :_, :foo) - assert Registry.lookup(registry, :_) == [{self(), :bar}] + test "unregister_match supports tricky keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, :foo) + {:ok, _} = Registry.register(registry, :_, :bar) + {:ok, _} = Registry.register(registry, "hello", "a") + {:ok, _} = Registry.register(registry, "hello", "b") - assert Registry.keys(registry, self()) |> Enum.sort() == [:_, "hello", "hello"] - end + Registry.unregister_match(registry, :_, :foo) + assert Registry.lookup(registry, :_) == [{self(), :bar}] - @tag listener: :"duplicate_listener_#{partitions}" - test "allows listeners", %{registry: registry, listener: listener} do - Process.register(self(), listener) - {_, task} = register_task(registry, "hello", :world) - assert_received {:register, ^registry, "hello", ^task, :world} + assert Registry.keys(registry, self()) |> Enum.sort() == [:_, "hello", "hello"] + end - self = self() - {:ok, _} = Registry.register(registry, "hello", :value) - assert_received {:register, ^registry, "hello", ^self, :value} + @tag base_listener: :unique_listener + test "allows listeners", %{registry: registry, listeners: [listener]} do + Process.register(self(), listener) + {_, task} = register_task(registry, "hello", :world) + assert_received {:register, ^registry, "hello", ^task, :world} - :ok = Registry.unregister(registry, "hello") - assert_received {:unregister, ^registry, "hello", ^self} - end + self = self() + {:ok, _} = Registry.register(registry, "hello", :value) + assert_received {:register, ^registry, "hello", ^self, :value} - test "links and unlinks on register/unregister", %{registry: registry} do - {:ok, pid} = Registry.register(registry, "hello", :value) - {:links, links} = Process.info(self(), :links) - assert pid in links + :ok = Registry.unregister(registry, "hello") + assert_received {:unregister, ^registry, "hello", ^self} + after + Process.unregister(listener) + end - {:ok, pid} = Registry.register(registry, "world", :value) - {:links, links} = Process.info(self(), :links) - assert pid in links + test "links and unlinks on register/unregister", %{registry: registry} do + {:ok, pid} = Registry.register(registry, "hello", :value) + {:links, links} = Process.info(self(), :links) + assert pid in links - :ok = Registry.unregister(registry, "hello") - {:links, links} = Process.info(self(), :links) - assert pid in links + {:ok, pid} = Registry.register(registry, "world", :value) + {:links, links} = Process.info(self(), :links) + assert pid in links - :ok = Registry.unregister(registry, "world") - {:links, links} = Process.info(self(), :links) - refute pid in links - end + :ok = Registry.unregister(registry, "hello") + {:links, links} = Process.info(self(), :links) + assert pid in links - test "raises on unknown registry name" do - assert_raise ArgumentError, ~r/unknown registry/, fn -> - Registry.register(:unknown, "hello", :value) - end - end + :ok = Registry.unregister(registry, "world") + {:links, links} = Process.info(self(), :links) + refute pid in links + end - test "raises if attempt to be used on via", %{registry: registry} do - assert_raise ArgumentError, ":via is not supported for duplicate registries", fn -> - name = {:via, Registry, {registry, "hello"}} - Agent.start_link(fn -> 0 end, name: name) - end + test "raises on unknown registry name" do + assert_raise ArgumentError, ~r/unknown registry/, fn -> + Registry.register(:unknown, "hello", :value) end + end - test "empty list for empty registry", %{registry: registry} do - assert Registry.select(registry, [{{:_, :_, :_}, [], [:"$_"]}]) == [] + test "raises if attempt to be used on via", %{registry: registry} do + assert_raise ArgumentError, ":via is not supported for duplicate registries", fn -> + name = {:via, Registry, {registry, "hello"}} + Agent.start_link(fn -> 0 end, name: name) end + end - test "select all", %{registry: registry} do - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "hello", :value) + test "empty list for empty registry", %{registry: registry} do + assert Registry.select(registry, [{{:_, :_, :_}, [], [:"$_"]}]) == [] + end - assert Registry.select(registry, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) - |> Enum.sort() == - [{"hello", self(), :value}, {"hello", self(), :value}] - end + test "select all", %{registry: registry} do + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "hello", :value) - test "select supports full match specs", %{registry: registry} do - value = {1, :atom, 1} - {:ok, _} = Registry.register(registry, "hello", value) + assert Registry.select(registry, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) + |> Enum.sort() == + [{"hello", self(), :value}, {"hello", self(), :value}] + end - assert [{"hello", self(), value}] == - Registry.select(registry, [ - {{"hello", :"$2", :"$3"}, [], [{{"hello", :"$2", :"$3"}}]} - ]) + test "select supports full match specs", %{registry: registry} do + value = {1, :atom, 1} + {:ok, _} = Registry.register(registry, "hello", value) - assert [{"hello", self(), value}] == - Registry.select(registry, [ - {{:"$1", self(), :"$3"}, [], [{{:"$1", self(), :"$3"}}]} - ]) + assert [{"hello", self(), value}] == + Registry.select(registry, [ + {{"hello", :"$2", :"$3"}, [], [{{"hello", :"$2", :"$3"}}]} + ]) - assert [{"hello", self(), value}] == - Registry.select(registry, [ - {{:"$1", :"$2", value}, [], [{{:"$1", :"$2", {value}}}]} - ]) + assert [{"hello", self(), value}] == + Registry.select(registry, [ + {{:"$1", self(), :"$3"}, [], [{{:"$1", self(), :"$3"}}]} + ]) - assert [] == - Registry.select(registry, [ - {{"world", :"$2", :"$3"}, [], [{{"world", :"$2", :"$3"}}]} - ]) + assert [{"hello", self(), value}] == + Registry.select(registry, [ + {{:"$1", :"$2", value}, [], [{{:"$1", :"$2", {value}}}]} + ]) - assert [] == Registry.select(registry, [{{:"$1", :"$2", {1.0, :_, :_}}, [], [:"$_"]}]) + assert [] == + Registry.select(registry, [ + {{"world", :"$2", :"$3"}, [], [{{"world", :"$2", :"$3"}}]} + ]) - assert [{"hello", self(), value}] == - Registry.select(registry, [ - {{:"$1", :"$2", {:"$3", :atom, :"$4"}}, [], - [{{:"$1", :"$2", {{:"$3", :atom, :"$4"}}}}]} - ]) + assert [] == Registry.select(registry, [{{:"$1", :"$2", {1.0, :_, :_}}, [], [:"$_"]}]) - assert [{"hello", self(), {1, :atom, 1}}] == - Registry.select(registry, [ - {{:"$1", :"$2", {:"$3", :"$4", :"$3"}}, [], - [{{:"$1", :"$2", {{:"$3", :"$4", :"$3"}}}}]} - ]) + assert [{"hello", self(), value}] == + Registry.select(registry, [ + {{:"$1", :"$2", {:"$3", :atom, :"$4"}}, [], + [{{:"$1", :"$2", {{:"$3", :atom, :"$4"}}}}]} + ]) - value2 = %{a: "a", b: "b"} - {:ok, _} = Registry.register(registry, "world", value2) + assert [{"hello", self(), {1, :atom, 1}}] == + Registry.select(registry, [ + {{:"$1", :"$2", {:"$3", :"$4", :"$3"}}, [], + [{{:"$1", :"$2", {{:"$3", :"$4", :"$3"}}}}]} + ]) - assert [:match] == - Registry.select(registry, [{{"world", self(), %{b: "b"}}, [], [:match]}]) + value2 = %{a: "a", b: "b"} + {:ok, _} = Registry.register(registry, "world", value2) - assert ["hello", "world"] == - Registry.select(registry, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort() - end + assert [:match] == + Registry.select(registry, [{{"world", self(), %{b: "b"}}, [], [:match]}]) - test "select supports guard conditions", %{registry: registry} do - value = {1, :atom, 2} - {:ok, _} = Registry.register(registry, "hello", value) - - assert [{"hello", self(), {1, :atom, 2}}] == - Registry.select(registry, [ - {{:"$1", :"$2", {:"$3", :"$4", :"$5"}}, [{:>, :"$5", 1}], - [{{:"$1", :"$2", {{:"$3", :"$4", :"$5"}}}}]} - ]) - - assert [] == - Registry.select(registry, [ - {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [:"$_"]} - ]) - - assert ["hello"] == - Registry.select(registry, [ - {{:"$1", :_, {:_, :"$2", :_}}, [{:is_atom, :"$2"}], [:"$1"]} - ]) - end + assert ["hello", "world"] == + Registry.select(registry, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort() + end - test "select allows multiple specs", %{registry: registry} do - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "world", :value) + test "select supports guard conditions", %{registry: registry} do + value = {1, :atom, 2} + {:ok, _} = Registry.register(registry, "hello", value) + + assert [{"hello", self(), {1, :atom, 2}}] == + Registry.select(registry, [ + {{:"$1", :"$2", {:"$3", :"$4", :"$5"}}, [{:>, :"$5", 1}], + [{{:"$1", :"$2", {{:"$3", :"$4", :"$5"}}}}]} + ]) + + assert [] == + Registry.select(registry, [ + {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [:"$_"]} + ]) + + assert ["hello"] == + Registry.select(registry, [ + {{:"$1", :_, {:_, :"$2", :_}}, [{:is_atom, :"$2"}], [:"$1"]} + ]) + end - assert ["hello", "world"] == - Registry.select(registry, [ - {{"hello", :_, :_}, [], [{:element, 1, :"$_"}]}, - {{"world", :_, :_}, [], [{:element, 1, :"$_"}]} - ]) - |> Enum.sort() - end + test "select allows multiple specs", %{registry: registry} do + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "world", :value) - test "count_select supports match specs", %{registry: registry} do - value = {1, :atom, 1} - {:ok, _} = Registry.register(registry, "hello", value) - assert 1 == Registry.count_select(registry, [{{:_, :_, value}, [], [true]}]) - assert 1 == Registry.count_select(registry, [{{"hello", :_, :_}, [], [true]}]) - assert 1 == Registry.count_select(registry, [{{:_, :_, {1, :atom, :_}}, [], [true]}]) - assert 1 == Registry.count_select(registry, [{{:_, :_, {:"$1", :_, :"$1"}}, [], [true]}]) - assert 0 == Registry.count_select(registry, [{{"hello", :_, nil}, [], [true]}]) - - value2 = %{a: "a", b: "b"} - {:ok, _} = Registry.register(registry, "world", value2) - assert 1 == Registry.count_select(registry, [{{"world", :_, :_}, [], [true]}]) - end + assert ["hello", "world"] == + Registry.select(registry, [ + {{"hello", :_, :_}, [], [{:element, 1, :"$_"}]}, + {{"world", :_, :_}, [], [{:element, 1, :"$_"}]} + ]) + |> Enum.sort() + end - test "count_select supports guard conditions", %{registry: registry} do - value = {1, :atom, 2} - {:ok, _} = Registry.register(registry, "hello", value) + test "count_select supports match specs", %{registry: registry} do + value = {1, :atom, 1} + {:ok, _} = Registry.register(registry, "hello", value) + assert 1 == Registry.count_select(registry, [{{:_, :_, value}, [], [true]}]) + assert 1 == Registry.count_select(registry, [{{"hello", :_, :_}, [], [true]}]) + assert 1 == Registry.count_select(registry, [{{:_, :_, {1, :atom, :_}}, [], [true]}]) + assert 1 == Registry.count_select(registry, [{{:_, :_, {:"$1", :_, :"$1"}}, [], [true]}]) + assert 0 == Registry.count_select(registry, [{{"hello", :_, nil}, [], [true]}]) + + value2 = %{a: "a", b: "b"} + {:ok, _} = Registry.register(registry, "world", value2) + assert 1 == Registry.count_select(registry, [{{"world", :_, :_}, [], [true]}]) + end - assert 1 == - Registry.count_select(registry, [ - {{:_, :_, {:_, :"$1", :_}}, [{:is_atom, :"$1"}], [true]} - ]) + test "count_select supports guard conditions", %{registry: registry} do + value = {1, :atom, 2} + {:ok, _} = Registry.register(registry, "hello", value) - assert 1 == - Registry.count_select(registry, [ - {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 1}], [true]} - ]) + assert 1 == + Registry.count_select(registry, [ + {{:_, :_, {:_, :"$1", :_}}, [{:is_atom, :"$1"}], [true]} + ]) - assert 0 == - Registry.count_select(registry, [ - {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [true]} - ]) - end + assert 1 == + Registry.count_select(registry, [ + {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 1}], [true]} + ]) + + assert 0 == + Registry.count_select(registry, [ + {{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [true]} + ]) + end - test "count_select allows multiple specs", %{registry: registry} do - {:ok, _} = Registry.register(registry, "hello", :value) - {:ok, _} = Registry.register(registry, "world", :value) + test "count_select allows multiple specs", %{registry: registry} do + {:ok, _} = Registry.register(registry, "hello", :value) + {:ok, _} = Registry.register(registry, "world", :value) - assert 2 == - Registry.count_select(registry, [ - {{"hello", :_, :_}, [], [true]}, - {{"world", :_, :_}, [], [true]} - ]) - end + assert 2 == + Registry.count_select(registry, [ + {{"hello", :_, :_}, [], [true]}, + {{"world", :_, :_}, [], [true]} + ]) end end # Note: those tests relies on internals for keys <- [:unique, :duplicate] do - describe "clean up #{keys} registry on process crash" do - @describetag keys: keys - - @tag partitions: 8 - test "with 8 partitions", %{registry: registry} do - {_, task1} = register_task(registry, "hello", :value) - {_, task2} = register_task(registry, "world", :value) - - kill_and_assert_down(task1) - kill_and_assert_down(task2) - - # pid might be in different partition to key so need to sync with all - # partitions before checking ETS tables are empty. - for i <- 0..7 do + @tag keys: keys + test "clean up #{keys} registry on process crash", + %{registry: registry, partitions: partitions} do + {_, task1} = register_task(registry, "hello", :value) + {_, task2} = register_task(registry, "world", :value) + + kill_and_assert_down(task1) + kill_and_assert_down(task2) + + # pid might be in different partition to key so need to sync with all + # partitions before checking ETS tables are empty. + if partitions > 1 do + for i <- 0..(partitions - 1) do [{_, _, {partition, _}}] = :ets.lookup(registry, i) GenServer.call(partition, :sync) end - for i <- 0..7 do + for i <- 0..(partitions - 1) do [{_, key, {_, pid}}] = :ets.lookup(registry, i) assert :ets.tab2list(key) == [] assert :ets.tab2list(pid) == [] end - end - - @tag partitions: 1 - test "with 1 partition", %{registry: registry} do - {_, task1} = register_task(registry, "hello", :value) - {_, task2} = register_task(registry, "world", :value) - - kill_and_assert_down(task1) - kill_and_assert_down(task2) - + else [{-1, {_, _, key, {partition, pid}, _}}] = :ets.lookup(registry, -1) GenServer.call(partition, :sync) assert :ets.tab2list(key) == [] @@ -962,50 +962,6 @@ defmodule RegistryTest do end end - test "child_spec/1 uses :name as :id" do - assert %{id: :custom_name} = Registry.child_spec(name: :custom_name) - assert %{id: Registry} = Registry.child_spec([]) - end - - test "raises if :name is missing" do - assert_raise ArgumentError, ~r/expected :name option to be present/, fn -> - Registry.start_link(keys: :unique) - end - end - - test "raises if :name is not an atom" do - assert_raise ArgumentError, ~r/expected :name to be an atom, got/, fn -> - Registry.start_link(keys: :unique, name: []) - end - end - - test "raises if :compressed is not a boolean" do - assert_raise ArgumentError, ~r/expected :compressed to be a boolean, got/, fn -> - Registry.start_link(keys: :unique, name: :name, compressed: :fail) - end - end - - test "unregistration on crash with {registry, key, value} via tuple", %{registry: registry} do - name = {:via, Registry, {registry, :name, :value}} - spec = %{id: :foo, start: {Agent, :start_link, [fn -> raise "some error" end, [name: name]]}} - assert {:error, {error, _childspec}} = start_supervised(spec) - assert {%RuntimeError{message: "some error"}, _stacktrace} = error - end - - test "send works", %{registry: registry} do - name = {registry, "self"} - Registry.register_name(name, self()) - GenServer.cast({:via, Registry, name}, :message) - assert_received {:"$gen_cast", :message} - end - - test "send works with value", %{registry: registry} do - name = {registry, "self", "value"} - Registry.register_name(name, self()) - GenServer.cast({:via, Registry, name}, :message) - assert_received {:"$gen_cast", :message} - end - defp register_task(registry, key, value) do parent = self() diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex index 8ba9379b96a..62ff91275a3 100644 --- a/lib/ex_unit/lib/ex_unit.ex +++ b/lib/ex_unit/lib/ex_unit.ex @@ -101,9 +101,10 @@ defmodule ExUnit do * `:time` - the duration in microseconds of the test's runtime * `:tags` - the test tags * `:logs` - the captured logs + * `:parameters` - the test parameters """ - defstruct [:name, :case, :module, :state, time: 0, tags: %{}, logs: ""] + defstruct [:name, :case, :module, :state, time: 0, tags: %{}, logs: "", parameters: %{}] # TODO: Remove the `:case` field on v2.0 @type t :: %__MODULE__{ @@ -131,8 +132,10 @@ defmodule ExUnit do * `:tests` - all tests in this module + * `:parameters` - the test module parameters + """ - defstruct [:file, :name, :state, tags: %{}, tests: []] + defstruct [:file, :name, :state, tags: %{}, tests: [], parameters: %{}] @type t :: %__MODULE__{ file: binary(), @@ -404,10 +407,12 @@ defmodule ExUnit do for module <- additional_modules do module_attributes = module.__info__(:attributes) - if true in Keyword.get(module_attributes, :ex_unit_async, []) do - ExUnit.Server.add_async_module(module) - else - ExUnit.Server.add_sync_module(module) + case Keyword.get(module_attributes, :ex_unit_module) do + [config] -> + ExUnit.Server.add_module(module, config) + + _ -> + raise(ArgumentError, "#{inspect(module)} is not a ExUnit.Case module") end end diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex index d8aa44dfe2d..3295ebaf32b 100644 --- a/lib/ex_unit/lib/ex_unit/case.ex +++ b/lib/ex_unit/lib/ex_unit/case.ex @@ -15,6 +15,11 @@ defmodule ExUnit.Case do * `:register` - when `false`, does not register this module within ExUnit server. This means the module won't run when ExUnit suite runs. + * `:parameterize` - a list of maps to parameterize tests. If both + `:async` and `:parameterize` are given, the different parameters + run concurrently. See the "Parameterized tests" section below for + more information. + > #### `use ExUnit.Case` {: .info} > > When you `use ExUnit.Case`, it will import the functionality @@ -173,6 +178,41 @@ defmodule ExUnit.Case do * `:tmp_dir` - (since v1.11.0) see the "Tmp Dir" section below + ## Parameterized tests + + Sometimes you want to run the same tests but with different parameters. + In ExUnit, it is possible to do so by passing a `:parameterize` key to + `ExUnit.Case`. The value must be a list of maps which will be the + parameters merged into the test context. + + For example, Elixir has a module called `Registry`, which can have type + `:unique` or `:duplicate`, and can control its concurrency factor using + the `:partitions` option. If you have a number of tests that *behave the + same* across all of those values, you can parameterize those tests with: + + use ExUnit.Case, + async: true, + parameterize: + for(kind <- [:unique, :duplicate], + partitions <- [1, 8], + do: %{kind: kind, partitions: partitions}) + + Then, in your tests, you can access the parameters as part of the context: + + test "starts a registry", %{kind: kind, partitions: partitions} do + ... + end + + Use parameterized tests with care: + + * Although parameterized tests run concurrently when `async: true` is also given, + abuse of parameterized tests may make your test suite slower + + * If you use parameterized tests and then find yourself adding conditionals + in your tests to deal with different parameters, then parameterized tests + may be the wrong solution to your problem. Consider creating separated + tests and sharing logic between them using regular functions + ## Filters Tags can also be used to identify specific tests, which can then @@ -278,6 +318,18 @@ defmodule ExUnit.Case do ~s(got: #{inspect(opts)}) end + {register?, opts} = Keyword.pop(opts, :register, true) + {async?, opts} = Keyword.pop(opts, :async, false) + {parameterize, opts} = Keyword.pop(opts, :parameterize, nil) + + unless parameterize == nil or (is_list(parameterize) and Enum.all?(parameterize, &is_map/1)) do + raise ArgumentError, ":parameterize must be a list of maps, got: #{inspect(parameterize)}" + end + + if opts != [] do + IO.warn("unknown options given to ExUnit.Case: #{inspect(opts)}") + end + registered? = Module.has_attribute?(module, :ex_unit_tests) unless registered? do @@ -299,23 +351,18 @@ defmodule ExUnit.Case do Enum.each(accumulate_attributes, &Module.register_attribute(module, &1, accumulate: true)) - persisted_attributes = [:ex_unit_async] + persisted_attributes = [:ex_unit_module] Enum.each(persisted_attributes, &Module.register_attribute(module, &1, persist: true)) - if Keyword.get(opts, :register, true) do + if register? do Module.put_attribute(module, :after_compile, ExUnit.Case) end Module.put_attribute(module, :before_compile, ExUnit.Case) end - async? = opts[:async] - - if is_boolean(async?) or not registered? do - Module.put_attribute(module, :ex_unit_async, async? || false) - end - + Module.put_attribute(module, :ex_unit_module, {async?, parameterize}) registered? end @@ -498,21 +545,22 @@ defmodule ExUnit.Case do end @doc false - defmacro __before_compile__(env) do + defmacro __before_compile__(%{module: module} = env) do tests = - env.module + module |> Module.get_attribute(:ex_unit_tests) |> Enum.reverse() |> Macro.escape() - moduletag = Module.get_attribute(env.module, :moduletag) + moduletag = Module.get_attribute(module, :moduletag) + {async?, _parameterize} = Module.get_attribute(module, :ex_unit_module) tags = moduletag |> normalize_tags() |> validate_tags() |> Map.new() - |> Map.merge(%{module: env.module, case: env.module}) + |> Map.merge(%{module: module, case: env.module, async: async?}) quote do def __ex_unit__ do @@ -529,17 +577,16 @@ defmodule ExUnit.Case do @doc false def __after_compile__(%{module: module}, _) do cond do - Process.whereis(ExUnit.Server) == nil -> - unless Code.can_await_module_compilation?() do - raise "cannot use ExUnit.Case without starting the ExUnit application, " <> - "please call ExUnit.start() or explicitly start the :ex_unit app" - end + Process.whereis(ExUnit.Server) -> + config = Module.get_attribute(module, :ex_unit_module) + ExUnit.Server.add_module(module, config) - Module.get_attribute(module, :ex_unit_async) -> - ExUnit.Server.add_async_module(module) + Code.can_await_module_compilation?() -> + :ok true -> - ExUnit.Server.add_sync_module(module) + raise "cannot use ExUnit.Case without starting the ExUnit application, " <> + "please call ExUnit.start() or explicitly start the :ex_unit app" end end @@ -577,7 +624,6 @@ defmodule ExUnit.Case do moduletag = Module.get_attribute(mod, :moduletag) tag = Module.delete_attribute(mod, :tag) - async = Module.get_attribute(mod, :ex_unit_async) {name, describe, describe_line, describetag} = case Module.get_attribute(mod, :ex_unit_describe) do @@ -602,7 +648,6 @@ defmodule ExUnit.Case do line: line, file: file, registered: registered, - async: async, describe: describe, describe_line: describe_line, test_type: test_type diff --git a/lib/ex_unit/lib/ex_unit/cli_formatter.ex b/lib/ex_unit/lib/ex_unit/cli_formatter.ex index 67a1dda53dd..8bbd2bffbb8 100644 --- a/lib/ex_unit/lib/ex_unit/cli_formatter.ex +++ b/lib/ex_unit/lib/ex_unit/cli_formatter.ex @@ -137,9 +137,14 @@ defmodule ExUnit.CLIFormatter do {:noreply, update_test_timings(config, test)} end - def handle_cast({:module_started, %ExUnit.TestModule{name: name, file: file}}, config) do + def handle_cast({:module_started, %ExUnit.TestModule{} = module}, config) do if config.trace do + %{name: name, file: file, parameters: parameters} = module IO.puts("\n#{inspect(name)} [#{Path.relative_to_cwd(file)}]") + + if parameters != %{} do + IO.puts("Parameters: #{inspect(parameters)}") + end end {:noreply, config} diff --git a/lib/ex_unit/lib/ex_unit/formatter.ex b/lib/ex_unit/lib/ex_unit/formatter.ex index 2d87c2e4a65..8a35f38c68f 100644 --- a/lib/ex_unit/lib/ex_unit/formatter.ex +++ b/lib/ex_unit/lib/ex_unit/formatter.ex @@ -94,6 +94,7 @@ defmodule ExUnit.Formatter do | :error_info | :test_module_info | :test_info + | :parameters_info | :location_info | :stacktrace_info | :blame_diff @@ -119,7 +120,7 @@ defmodule ExUnit.Formatter do * `:diff_insert` and `:diff_insert_whitespace` - Should format a diff insertion, with or without whitespace respectively. - * `:extra_info` - Should format extra information, such as the `"code: "` label + * `:extra_info` - Should format optional extra labels, such as the `"code: "` label that precedes code to show. * `:error_info` - Should format error information. @@ -129,6 +130,8 @@ defmodule ExUnit.Formatter do * `:test_info` - Should format test information. + * `:parameters_info` - Should format test parameters. + * `:location_info` - Should format test location information. * `:stacktrace_info` - Should format stacktrace information. @@ -266,9 +269,10 @@ defmodule ExUnit.Formatter do ) :: String.t() when failure: {atom, term, Exception.stacktrace()} def format_test_failure(test, failures, counter, width, formatter) do - %ExUnit.Test{name: name, module: module, tags: tags} = test + %ExUnit.Test{name: name, module: module, tags: tags, parameters: parameters} = test test_info(with_counter(counter, "#{name} (#{inspect(module)})"), formatter) <> + test_parameters(parameters, formatter) <> test_location(with_location(tags), formatter) <> Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, index} -> {text, stack} = format_kind_reason(test, kind, reason, stack, width, formatter) @@ -305,9 +309,10 @@ defmodule ExUnit.Formatter do ) :: String.t() when failure: {atom, term, Exception.stacktrace()} def format_test_all_failure(test_module, failures, counter, width, formatter) do - name = test_module.name + %{name: name, parameters: parameters} = test_module test_module_info(with_counter(counter, "#{inspect(name)}: "), formatter) <> + test_parameters(parameters, formatter) <> Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, index} -> {text, stack} = format_kind_reason(test_module, kind, reason, stack, width, formatter) failure_header(failures, index) <> text <> format_stacktrace(stack, name, nil, formatter) @@ -711,6 +716,15 @@ defmodule ExUnit.Formatter do defp test_info(msg, nil), do: msg <> "\n" defp test_info(msg, formatter), do: test_info(formatter.(:test_info, msg), nil) + defp test_parameters(params, _formatter) when params == %{}, do: "" + defp test_parameters(params, nil) when is_binary(params), do: " " <> params <> "\n" + + defp test_parameters(params, nil) when is_map(params), + do: test_parameters("Parameters: #{inspect(params)}", nil) + + defp test_parameters(params, formatter), + do: test_parameters(formatter.(:parameters_info, params), nil) + defp test_location(msg, nil), do: " " <> msg <> "\n" defp test_location(msg, formatter), do: test_location(formatter.(:location_info, msg), nil) diff --git a/lib/ex_unit/lib/ex_unit/runner.ex b/lib/ex_unit/lib/ex_unit/runner.ex index 08d27d129bf..1352960a7c1 100644 --- a/lib/ex_unit/lib/ex_unit/runner.ex +++ b/lib/ex_unit/lib/ex_unit/runner.ex @@ -106,9 +106,9 @@ defmodule ExUnit.Runner do async_loop(config, running, async_once?, modules_to_restore) # Slots are available, start with async modules - modules = ExUnit.Server.take_async_modules(available) -> - running = spawn_modules(config, modules, running) - modules_to_restore = maybe_store_modules(modules_to_restore, :async, modules) + async_modules = ExUnit.Server.take_async_modules(available) -> + running = spawn_modules(config, async_modules, running) + modules_to_restore = maybe_store_modules(modules_to_restore, :async, async_modules) async_loop(config, running, true, modules_to_restore) true -> @@ -124,8 +124,8 @@ defmodule ExUnit.Runner do async_stop_time = if async_once?, do: System.monotonic_time(), else: nil # Run all sync modules directly - for module <- sync_modules do - running = spawn_modules(config, [module], %{}) + for pair <- sync_modules do + running = spawn_modules(config, [pair], %{}) running != %{} and wait_until_available(config, running) end @@ -161,11 +161,11 @@ defmodule ExUnit.Runner do running end - defp spawn_modules(config, [module | modules], running) do + defp spawn_modules(config, [{module, params} | modules], running) do if max_failures_reached?(config) do running else - {pid, ref} = spawn_monitor(fn -> run_module(config, module) end) + {pid, ref} = spawn_monitor(fn -> run_module(config, module, params) end) spawn_modules(config, modules, Map.put(running, ref, pid)) end end @@ -221,8 +221,8 @@ defmodule ExUnit.Runner do ## Running modules - defp run_module(config, module) do - test_module = module.__ex_unit__() + defp run_module(config, module, params) do + test_module = %{module.__ex_unit__() | parameters: params} EM.module_started(config.manager, test_module) # Prepare tests, selecting which ones should be run or skipped @@ -233,7 +233,8 @@ defmodule ExUnit.Runner do EM.test_finished(config.manager, excluded_or_skipped_test) end - {test_module, invalid_tests, finished_tests} = run_module(config, test_module, to_run_tests) + {test_module, invalid_tests, finished_tests} = + run_module_tests(config, test_module, to_run_tests) pending_tests = case process_max_failures(config, test_module) do @@ -284,18 +285,20 @@ defmodule ExUnit.Runner do test_ids == nil or MapSet.member?(test_ids, {test.module, test.name}) end - defp run_module(_config, test_module, []) do + defp run_module_tests(_config, test_module, []) do {test_module, [], []} end - defp run_module(config, test_module, tests) do + defp run_module_tests(config, test_module, tests) do {module_pid, module_ref} = run_setup_all(test_module, self()) {test_module, invalid_tests, finished_tests} = receive do {^module_pid, :setup_all, {:ok, context}} -> finished_tests = - if max_failures_reached?(config), do: [], else: run_tests(config, tests, context) + if max_failures_reached?(config), + do: [], + else: run_tests(config, tests, test_module.parameters, context) :ok = exit_setup_all(module_pid, module_ref) {test_module, [], finished_tests} @@ -319,7 +322,10 @@ defmodule ExUnit.Runner do Enum.map(tests, &%{&1 | state: {:invalid, test_module}}) end - defp run_setup_all(%ExUnit.TestModule{name: module} = test_module, parent_pid) do + defp run_setup_all( + %ExUnit.TestModule{name: module, tags: tags, parameters: params} = test_module, + parent_pid + ) do Process.put(@current_key, test_module) spawn_monitor(fn -> @@ -327,7 +333,7 @@ defmodule ExUnit.Runner do result = try do - {:ok, module.__ex_unit__(:setup_all, test_module.tags)} + {:ok, module.__ex_unit__(:setup_all, Map.merge(tags, params))} catch kind, error -> failed = failed(kind, error, prune_stacktrace(__STACKTRACE__)) @@ -355,8 +361,9 @@ defmodule ExUnit.Runner do end end - defp run_tests(config, tests, context) do + defp run_tests(config, tests, params, context) do Enum.reduce_while(tests, [], fn test, acc -> + test = %{test | parameters: params} Process.put(@current_key, test) case run_test(config, test, context) do @@ -401,14 +408,15 @@ defmodule ExUnit.Runner do spawn_monitor(fn -> ExUnit.OnExitHandler.register(self()) generate_test_seed(seed, test, rand_algorithm) - capture_log = Map.get(test.tags, :capture_log, capture_log) + context = Map.merge(context, test.tags) + capture_log = Map.get(context, :capture_log, capture_log) {time, test} = :timer.tc( maybe_capture_log(capture_log, test, fn -> - tags = maybe_create_tmp_dir(test.tags, test) + context = maybe_create_tmp_dir(context, test) - case exec_test_setup(test, Map.merge(context, tags)) do + case exec_test_setup(test, context) do {:ok, context} -> exec_test(test, context) {:error, test} -> test end diff --git a/lib/ex_unit/lib/ex_unit/server.ex b/lib/ex_unit/lib/ex_unit/server.ex index c7ea7211449..e44dfd5d1c8 100644 --- a/lib/ex_unit/lib/ex_unit/server.ex +++ b/lib/ex_unit/lib/ex_unit/server.ex @@ -9,16 +9,20 @@ defmodule ExUnit.Server do GenServer.start_link(__MODULE__, :ok, name: @name) end - def add_async_module(name), do: add(name, :async) - def add_sync_module(name), do: add(name, :sync) + def add_module(name, {async?, parameterize}) do + modules = + if parameterize do + Enum.map(parameterize, &{name, &1}) + else + [{name, %{}}] + end - defp add(name, type) do - case GenServer.call(@name, {:add, name, type}, @timeout) do + case GenServer.call(@name, {:add, async?, modules}, @timeout) do :ok -> :ok :already_running -> - raise "cannot add #{type} case named #{inspect(name)} to test suite after the suite starts running" + raise "cannot add module named #{inspect(name)} to test suite after the suite starts running" end end @@ -97,19 +101,19 @@ defmodule ExUnit.Server do {:reply, diff, take_modules(%{state | loaded: :done})} end - def handle_call({:add, name, :async}, _from, %{loaded: loaded} = state) + def handle_call({:add, true, names}, _from, %{loaded: loaded} = state) when is_integer(loaded) do - state = update_in(state.async_modules, &[name | &1]) + state = update_in(state.async_modules, &(names ++ &1)) {:reply, :ok, take_modules(state)} end - def handle_call({:add, name, :sync}, _from, %{loaded: loaded} = state) + def handle_call({:add, false, names}, _from, %{loaded: loaded} = state) when is_integer(loaded) do - state = update_in(state.sync_modules, &[name | &1]) + state = update_in(state.sync_modules, &(names ++ &1)) {:reply, :ok, state} end - def handle_call({:add, _name, _type}, _from, state), + def handle_call({:add, _async?, _names}, _from, state), do: {:reply, :already_running, state} defp take_modules(%{waiting: nil} = state) do diff --git a/lib/ex_unit/test/ex_unit/case_test.exs b/lib/ex_unit/test/ex_unit/case_test.exs index 6325b5a91d0..8ed7a652fb1 100644 --- a/lib/ex_unit/test/ex_unit/case_test.exs +++ b/lib/ex_unit/test/ex_unit/case_test.exs @@ -161,34 +161,16 @@ defmodule ExUnit.CaseTest do end end -defmodule ExUnit.DoubleCaseTest1 do +defmodule ExUnit.DoubleCaseTestAsyncFirst do use ExUnit.Case, async: true use ExUnit.Case test "async must be true", context do - assert context.async - end -end - -defmodule ExUnit.DoubleCaseTest2 do - use ExUnit.Case, async: false - use ExUnit.Case - - test "async must be false", context do - refute context.async - end -end - -defmodule ExUnit.DoubleCaseTest3 do - use ExUnit.Case, async: true - use ExUnit.Case, async: false - - test "async must be false", context do refute context.async end end -defmodule ExUnit.DoubleCaseTest4 do +defmodule ExUnit.DoubleCaseTestAsyncLast do use ExUnit.Case use ExUnit.Case, async: true diff --git a/lib/ex_unit/test/ex_unit/formatter_test.exs b/lib/ex_unit/test/ex_unit/formatter_test.exs index bb9642fbcc4..39f07d56a8d 100644 --- a/lib/ex_unit/test/ex_unit/formatter_test.exs +++ b/lib/ex_unit/test/ex_unit/formatter_test.exs @@ -230,6 +230,31 @@ defmodule ExUnit.FormatterTest do """ end + test "formats test errors with parameters" do + failure = [{:error, catch_error(raise "oops"), []}] + + assert format_test_failure(%{test() | parameters: %{foo: :bar}}, failure, 1, 80, &formatter/2) =~ + """ + 1) world (Hello) + Parameters: %{foo: :bar} + test/ex_unit/formatter_test.exs:1 + ** (RuntimeError) oops + """ + + formatter = fn + :parameters_info, map -> Map.put(map, :more, :keys) + key, val -> formatter(key, val) + end + + assert format_test_failure(%{test() | parameters: %{foo: :bar}}, failure, 1, 80, formatter) =~ + """ + 1) world (Hello) + Parameters: #{inspect(%{foo: :bar, more: :keys})} + test/ex_unit/formatter_test.exs:1 + ** (RuntimeError) oops + """ + end + test "formats stacktraces" do stacktrace = [{Oops, :wrong, 1, [file: "formatter_test.exs", line: 1]}] failure = [{:error, catch_error(raise "oops"), stacktrace}] diff --git a/lib/ex_unit/test/ex_unit_test.exs b/lib/ex_unit/test/ex_unit_test.exs index 0b768f28e8e..9c795d704a0 100644 --- a/lib/ex_unit/test/ex_unit_test.exs +++ b/lib/ex_unit/test/ex_unit_test.exs @@ -683,6 +683,30 @@ defmodule ExUnitTest do assert output =~ "\n6 tests, 0 failures, 1 excluded, 4 invalid, 1 skipped\n" end + test "parameterized tests" do + defmodule ParameterizedTests do + use ExUnit.Case, async: true, parameterize: [%{value: true}, %{value: false}] + + test "hello world", %{value: value} do + assert value + end + end + + configure_and_reload_on_exit(trace: true) + + output = capture_io(fn -> ExUnit.run() end) + + assert output =~ """ + ExUnitTest.ParameterizedTests [test/ex_unit_test.exs] + Parameters: %{value: false} + """ + + assert output =~ """ + 1) test hello world (ExUnitTest.ParameterizedTests) + Parameters: %{value: false} + """ + end + describe "after_suite/1" do test "executes all callbacks set in reverse order" do Process.register(self(), :after_suite_test_process) @@ -1033,7 +1057,7 @@ defmodule ExUnitTest do ## Helpers defp run_with_filter(filters, cases) do - Enum.each(cases, &ExUnit.Server.add_sync_module/1) + Enum.each(cases, &ExUnit.Server.add_module(&1, {false, nil})) ExUnit.Server.modules_loaded(false) opts =