Creating a macro that forward quoted expression

I’m trying to write a macro to replace this:

test "expressions" do
  assert {:ok, true} = Compiler.eval(quote(do: gt(add(4, @age), 20)), %{age: 18})
  assert {:ok, 22} = Compiler.eval(quote(do: add(4, @age)), %{age: 18})
end

with this:

test "expressions with macro" do
  assert_expr(true, %{age: 18}) do
    gt(add(4, @age), 20)
  end
  assert_expr(22, %{age: 18}) do
    add(4, @age)
  end
end

But I must be stupid or tired or something, because I tried every possible combinations of quote/unquote I could think of, but I cannot get the macro to work (here is the general idea):

defmacro assert_expr(result, binding, do: block) do
  quote do
    # result should be matched like ^result if it was in a function, and block should be passed as AST
    assert {:ok, result} = Compiler.eval(block, binding)
  end
end

Looks like it’s about parametrization of the test with various %{age: ...} maps. Considering that test/3 is the macro by itself, it can be done like the following:

for {age, expected_sum} <- [{18, 22}, {19, 23}] do
  @age %{age: age}

  test "add #{inspect(@age)}" do
    assert add(4, @age) == unquote(expected_sum)
  end

  test "gt add #{inspect(@age)}" do
    assert gt(add(4, @age), 20) == true
  end
end
1 Like

I very much like @IvanR 's solution, and that’s probably the way to go in this particular example.

But to answer the question on how a macro like your assert_expr might be written: you probably need to use Macro.escape here:

defmacro assert_expr(result, binding, do: block) do
  quote do
    assert {:ok, unquote(result)} = Compiler.eval(unquote(Macro.escape(block)), unquote(binding))
  end
end

Or, using bind_quoted:, which is usually more readable than using unquote:

defmacro assert_expr(result, binding, do: block) do
  quote bind_quoted: [result: result, binding: Macro.escape(binding), block: Macro.escape(block)] do
    assert {:ok, result} = Compiler.eval(block, binding)
  end
end
2 Likes

Actually, I might go the route suggested by @IvanR but knowing about Macro.escape is really nice, I didn’t think about it. Thanks.

1 Like