Today I learnt that it is forbidden to use an attribute which is a port or a (reference) in an ExUnit test. (Pids are just fine).

defmodule ScratchpadTest do
  use ExUnit.Case
  @port :erlang.list_to_port('#Port<0.1234>')

  test "porty worty" do
    @port
  end
end

Gets you

** (ArgumentError) cannot inject attribute @port into function/macro because 
cannot escape #Port<0.1234>.  The supported values are: lists, tuples, maps, 
atoms, numbers, bitstrings, PIDs and remote functions in the format &Mod.fun/arity

It is, of course, super-easy to work around:

defmodule ScratchpadTest do
  use ExUnit.Case

  test "porty worty" do
    assert is_port(:erlang.list_to_port('#Port<0.1234>'))
  end
end

You might, as I did, think the limitation was caused solely by test/2 being a macro and I suppose it kind-of is. But it is easy to forget that Macros are everywhere in Elixir. This does not work.

defmodule ScratchpadTest do
  use ExUnit.Case
  @port :erlang.list_to_port('#Port<0.1234>')

  test "porty worty" do
    assert is_port(port())
  end

  defp port, do: @port
end

defp/2 and def/2 are also macros. This too does not compile

defmodule Scratchpad do
  @port :erlang.list_to_port('#Port<0.1234>')
  def port, do: @port
end

Weirdly though this does compile, even though defmodule/2 is a macro.

defmodule Scratchpad do
  @port :erlang.list_to_port('#Port<0.1234>')
  IO.inspect(@port)
end

No doubt if I was more learned, or put the research in, I would understand why. None of this is that important so

¯\(°_o)/¯☕️

Anyway, if you have read this far sorry (not sorry) for you learning nothing useful. I’m going back to some crazy test-driving of websocket client stuff using Mint Websocket.


Update 2023-03022 Late update: ubiquitous and helpful Elixir Forum poster Benjamin Milde explained the problem is the way Elixir macros work: they essentially work by injecting the macro AST at the point in the code AST. As ports and reference values can not be seralised to AST then things fail. That makes sense in terms of mechanism and that there is no good reason for passing ports or references from compile time to runtime. (Although I now do wonder why PIDs are AST serialisable).

The actual issue is between compile and runtime, not really macros, which is obvious now I think about it. For example, this does not comile

defmodule Scratchpad do
  @port :erlang.list_to_port('#Port<0.1234>')
  def port, do: @port
end

I should really rewrite this post with that emphasis.