コードの生成に module attributes を使う、とのこと。翻訳時にモジュールに格納されてるデータにアクセスできる、なのかどうか。
accumulate:true
というオプションが attribute を登録する時に云々、とあります。
test macro
二つの argument を受け取る、とのこと。
- string description (キーワードリスト?)
- do/end なコードブロック
コード例示
以下で良いのかどうか。
defmodule Assertion do
defmacro __using__(_options) do
quote do
import unquote(__MODULE__)
Module.register_attribute __MODULE__, :tests, accumulate: true
def run do
IO.puts "Running the tests (#{inspect @tests})"
end
end
end
defmacro test(description, do: test_block) do
test_func = String.to_atom(description)
quote do
@tests {unquote(test_func), unquote(description)}
def unquote(test_func)(), do: unquote(test_block)
end
end
end
これ
- Module.register_attribute で tests という attribute を登録
- @test という attribute を inspect
しているとのこと。次に test という macro を定義してるのですがこれは一体何なのかと。ちょっと整理してみます。
use Assertion
で__using__
なマクロが展開- tests て attribute が登録される
- tests を inspect して出力する run という手続きが定義される
- test というマクロは展開されたら caller’s context で動作?
- @tests に test_func (description を atom にしたもの) と description の参照を設定?
def unquote(test_func)(), do: unquote(test_block)
の意味をわかりかねています
たぶんこれはまだ足りてないはず。
あら?
なんかおかしいと思ったら assert_step2.exs に追加なのか。以下を assertion.exs としてでっちあげて
defmodule Assertion do
defmacro __using__(_options) do
quote do
import unquote(__MODULE__)
Module.register_attribute __MODULE__, :tests, accumulate: true
def run do
IO.puts "Running the tests (#{inspect @tests})"
end
end
end
defmacro test(description, do: test_block) do
test_func = String.to_atom(description)
quote do
@tests {unquote(test_func), unquote(description)}
def unquote(test_func)(), do: unquote(test_block)
end
end
# {:==, [context: Elixir, import: Kernel], [5, 5]}
defmacro assert({operator, _, [lhs, rhs]}) do
quote bind_quoted: [operator: operator, lhs: lhs, rhs: rhs] do
Assertion.Test.assert(operator, lhs, rhs)
end
end
end
defmodule Assertion.Test do
def assert(:==, lhs, rhs) when lhs == rhs do
IO.write "."
end
def assert(:==, lhs, rhs) do
IO.puts """
FAILURE:
Expected: #{lhs}
to be equal to #{rhs}
"""
end
def assert(:>, lhs, rhs) when lhs > rhs do
IO.write "."
end
def assert(:>, lhs, rhs) do
IO.puts"""
FAILURE:
Expected: #{lhs}
to be greater than: #{rhs}
"""
end
end
なんとなく同じ状態になったのかどうか。
iex(1)> c "assertion.exs"
[Assertion.Test, Assertion]
iex(2)> c "math_test_step1.exs"
[MathTest]
iex(3)> MathTest.__info__(:functions)
["integers can be added and subtracted": 0, run: 0]
iex(4)> MathTest.run
Running the tests ([])
:ok
で、これは何が起きたのかというと
- @tests attribute が空
- Assertion の using なブロックが微妙らしい
何が問題なのか、というと
- run/0 は tests 属性を登録した直後に定義
- run の定義は
use Assertion
の宣言で MathTest モジュール内で展開される - この時点で @tests は空
- test が eval される時点まで展開を遅らせたい
というこで
- before_compile fook が用意されている
とのこと。
compile-time hooks
@before_compile という特別な module attribute でコンパイルが終了する直前に追加の手順が必要であることをコンパイラに通知する、とのこと。また、@before_compile属性は、__before_compile __ / 1 マクロを定義する必要があるモジュール引数を受け入れる、とあります。これが追加の手順になるのだな。ちょっとテキストと微妙に差異があるのですが、before_compile.exs として以下を投入。
defmodule Assertion do
defmacro __using__(_options) do
quote do
import unquote(__MODULE__)
Module.register_attribute __MODULE__, :tests, accumulate: true
@before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(_env) do
quote do
def run do
IO.puts "Running the tests (#{inspect @tests})"
end
end
end
defmacro test(description, do: test_block) do
test_func = String.to_atom(description)
quote do
@tests {unquote(test_func), unquote(description)}
def unquote(test_func)(), do: unquote(test_block)
end
end
# {:==, [context: Elixir, import: Kernel], [5, 5]}
defmacro assert({operator, _, [lhs, rhs]}) do
quote bind_quoted: [operator: operator, lhs: lhs, rhs: rhs] do
Assertion.Test.assert(operator, lhs, rhs)
end
end
end
で、iex で以下。
iex(3)> c "before_compile.exs"
warning: redefining module Assertion (current version defined in memory)
before_compile.exs:1
[Assertion]
iex(4)> c "math_test_step1.exs"
[MathTest]
iex(5)> MathTest.run
Running the tests (["integers can be added and subtracted": "integers can be added and subtracted"])
:ok
またテストは動いていませんが @test の用意が終わった後に run という手続きが定義されたことがわかります。これを踏まえて assertion.exs を改良します。これが test framework になるのか。
defmodule Assertion do
defmacro __using__(_options) do
quote do
import unquote(__MODULE__)
Module.register_attribute __MODULE__, :tests, accumulate: true
@before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(_env) do
quote do
def run do
Assertion.Test.run(@tests, __MODULE__)
end
end
end
defmacro test(description, do: test_block) do
test_func = String.to_atom(description)
quote do
@tests {unquote(test_func), unquote(description)}
def unquote(test_func)(), do: unquote(test_block)
end
end
# {:==, [context: Elixir, import: Kernel], [5, 5]}
defmacro assert({operator, _, [lhs, rhs]}) do
quote bind_quoted: [operator: operator, lhs: lhs, rhs: rhs] do
Assertion.Test.assert(operator, lhs, rhs)
end
end
end
defmodule Assertion.Test do
def run(tests, module) do
Enum.each tests, fn {test_func, description} ->
case apply(module, test_func, []) do
:ok -> IO.write "."
{:fail, reason} -> IO.puts """
======================================
FAILURE: #{description}
======================================
#{reason}
"""
end
end
end
def assert(:==, lhs, rhs) when lhs == rhs do
:ok
end
def assert(:==, lhs, rhs) do
{:fail, """
Expected: #{lhs}
to be equal to: #{rhs}
"""
}
end
def assert(:>, lhs, rhs) when lhs > rhs do
:ok
end
def assert(:>, lhs, rhs) do
{:fail, """
Expected: #{lhs}
to be equal to: #{rhs}
"""
}
end
end
run の定義を直すのを忘れていたのはご愛嬌。で、試験は以下です。
defmodule MathTest do
use Assertion
test "integers can be added and subtracted" do
assert 2 + 3 == 5
assert 5 - 5 == 10
end
test "integers can be multiplied and divided" do
assert 5 * 5 == 25
assert 10 / 2 == 5
end
end
コンパイルして実行、が以下。
iex(1)> c "assertion.exs"
[Assertion.Test, Assertion]
iex(2)> c "math_test_final.exs"
[MathTest]
iex(3)> MathTest.run
.======================================
FAILURE: integers can be added and subtracted
======================================
Expected: 0
to be equal to: 10
:ok
次回エントリにて asertion.exs の最終形をべろべろしつつ復習の方向です。