How to test a macro by comparing the resulting AST?

Is there an existing way to compare the result of applying a particular macro by comparing the result with what the macro is supposed to expand it to?

defmacro twice(what) do
  quote do what || what end
end
# test:
assert_same_ast my_macro(:foo), :foo || :foo

I have to start:

  defmacrop assert_similar_ast(actual, expected) do
    actual = Macro.expand_once(actual, __CALLER__)
    quote do
      assert unquote(actual |> simplify_ast |> Macro.escape) ==
             unquote(expected |> simplify_ast |> Macro.escape)
    end
  end

But simplify_ast doesn’t seem super simple.

Any existing solution out there?

I’m confused as to why actual == expected doesn’t do what you are after? The difference would be in the metadata I suppose. But those differences might actually matter so would ignoring it give you any guarantees?

Assuming I’ve understood your requirements properly, this may help:

defmodule Mac7 do
  defmacro twice(what) do
    quote do
      unquote(what) || unquote(what)
    end
  end

  def prune_metadata({op, _meta, params}) do
    {op, [], params}
  end

  def prune_metadata(other) do
    other
  end
end

defmodule Mac7.Test do
  import Mac7

  defmacro test(actual, expected) do
    generated = Macro.expand_once(actual, __CALLER__)
    Macro.prewalk(generated, &prune_metadata/1) == Macro.prewalk(expected, &prune_metadata/1)
  end
end

The key is to use Macro.prewalk/2 to prune the metadata before comparison.

Example

iex> require Mac7.Test                            
Mac7.Test
iex> require Mac7                                 
Mac7
iex> Mac7.Test.test Mac7.twice(:foo), :foo || :foo
{:||, [], [:foo, :foo]}
{:||, [], [:foo, :foo]}
true
2 Likes

You can call a macro as a regular function using apply(module, :"MACRO-my_macro", [__ENV__, ...other args])

1 Like

Thanks @kip , prewalk helped make it easier <3

An ultimate solution would remove only the right metadata (like line number), but that works well for my purposes.

to add to @kip’s reply, Macro.update_meta/2 is really convenient for this:

iex> prune = &Macro.update_meta(&1, fn quoted -> Keyword.delete(quoted, :line) end)
iex> "1 + (2 * 3)" |> Code.string_to_quoted!() |> IO.inspect() |> Macro.prewalk(prune)
{:+, [line: 1], [1, {:*, [line: 1], [2, 3]}]}
{:+, [], [1, {:*, [], [2, 3]}]}
4 Likes

Perfect, even better, thanks @wojtekmach

1 Like

Oh that’s a gem how did I miss that

1 Like

Oh, forgot to mention, another idea is to do this:

assert Macro.to_string(qutoed1) == Macro.to_string(quoted2)

which tends to give a much nicer error message. A robust solution might combine both approaches.

2 Likes

Simpler and yes, even better than better in this case :heart:

1 Like