how to run a ETS before a test run? i got a: the table identifier does not refer to an existing ETS table error

i’m trying to run my tests but i got a : the table identifier does not refer to an existing ETS table error everytime.
what happens is we have a file inside phoenix called context.ex and there I have this piece of code:

defp store_user_in_ets(user_type) do
    :ets.new(:user_registry, [:set, :protected, :named_table])
    :ets.insert(:user_registry, {"user", user_type})
  end

then everytime a users login this file gets his data a passes to this function so we get if he is a regular user or a admin

then in myapp/audit_logs/audit_log.ex I have this code:

defp get_user_by_ets do
    user =
      case :ets.lookup_element(:user_registry, "user", 2) do
        %{current_admin_user: %{admin_user: user}} -> user
        [] -> {:erro, nil}
      end

    {:ok, user}
  end

and this I think where the problem happens in my tests:

test/data_layer/site_home_banners_test.exs:28
** (ArgumentError) errors were found at the given arguments:

   * 1st argument: the table identifier does not refer to an existing ETS table

 code: site_home_banner = site_home_banner_fixture()
 stacktrace:
   (stdlib 3.17.2) :ets.lookup_element(:user_registry, "user", 2)
   (data_layer 0.0.2) lib/data_layer/audit_logs/audit_log.ex:41: DataLayer.AuditLogs.AuditLog.get_user_by_ets/0
   (data_layer 0.0.2) lib/data_layer/audit_logs/audit_log.ex:10: DataLayer.AuditLogs.AuditLog.handle/3
   (data_layer 0.0.2) lib/data_layer/site_home_banners.ex:90: DataLayer.SiteHomeBanners.create_site_home_banner/2
   test/data_layer/site_home_banners_test.exs:23: DataLayer.SiteHomeBannersTest.site_home_banner_fixture/1
   test/data_layer/site_home_banners_test.exs:29: (test)

So you can put it in a Genserver and start Genserver in top of your test or in setup. or you can do somthing like this:

case ETS.Set.wrap_existing(@ets_table) do
      {:ok, set} -> set
      _ ->
        start_link([]) # or start your ets
        table()
    end
1 Like

so what this ETS.Set.wrap_existing does?

It is a warper of table info :ets.info(table), I am using GitHub - TheFirstAvenger/ets: :ets, the Elixir way library
ETS.Set — ets v0.9.0

If you are using :ets, you can call your table name or reference of the table based on your function. If table does not exist, so it tries to create it, and you can use setup and setup_all based on your requirement in unit test.

Check this out…

Open a new IEx repl and do this…

iex> :ets.new(:user_registry, [:set, :protected, :named_table])
:user_registry

iex> ets.info(:user_registry)
[
  id: #Reference<0.3400188531.1392902147.260966>,
  decentralized_counters: false,
  read_concurrency: false,
  write_concurrency: false,
  compressed: false,
  memory: 313,
  owner: #PID<0.341.0>,
  heir: :none,
  name: :user_registry,
  size: 0,
  node: :nonode@nohost,
  named_table: true,
  type: :set,
  keypos: 1,
  protection: :protected
]

Now open a new IEx repl and do this:

iex> spawn(fn -> :ets.new(:user_registry, [:set, :protected, :named_table]) end)
#PID<0.438.0>

iex> ets.info(:user_registry)
:undefined

Your store_user_in_ets function is creating the ETS table if it doesn’t exist. The problem is that it’s only alive as long as the process that spawned it is alive.

You should probably make a global :user_registry that is created when your app starts up.

Tests start a bunch of processes. Your :user_registry was created in an ephemeral process, thus when that ephemeral process ends, other processes don’t have access to :user_registry anymore.

2 Likes

Adding a little if OP is unfamiliar with where to start making a long running table.

You can start the table in the init/1 callback of a GenServer that will be the table owner. I like to wrap table access in an API that is defined in the same module as the GenServer.

defmodule UserRegistry do
  use GenServer

  ## API

  def store_user(user_type) do
    :ets.insert(__MODULE__, {"user", user_type})
  end

  # ... more API functions

  # the GenServer doesn't need a name because it will never be called
  def start_link(arg), do: GenServer.start_link(__MODULE__, arg)

  @impl true
  def init(_arg) do
    # using __MODULE__ as the name for convenience, table is only accessed by API functions defined here
    :ets.new(__MODULE__, [:set, :public, :named_table])
    {:ok, []}
  end

  @impl true
  # catch all unexpected messages and log them
  def handle_info(msg, state) do
    Logger.warn("UserRegistry received unexpected message: #{inspect(msg)}")
    {:noreply, state}
  end
end

If the process dies and restarts then so does the table, so the GenServer implementation is as thin as possible and all unexpected “regular” messages are handled by a catch-all callback. Access is public so you can still insert and lookup from any process, and in this case no access is performed by the owning process itself. Then you must add the UserRegistry GenServer to your supervision tree so the table always starts on application startup (including in tests) and (only) restarts in case of exception.

# lib/my_app/application.ex
def start(_args, _type) do
  children = [
    UserRegistry,
    ...
  ]
  
  opts = [...]
  Supervisor.start_link(children, opts)
end

Resources:
GenServer
ETS

1 Like