REnumというRubyやRailsのEnumerableMethodを使えるようにするライブラリを作っています。
このライブラリはメタプログラミングによりElixirのEnum全てのFunctionを移譲しております。
これらがうまく機能していることを担保する為、Elixi本体から各moduleの該当unittestをコピーしてきました。
ところがどっこい。test module内で定義されたstructでprotocolが上手く実装されてず、test自体も失敗してしまうという事象が発生しました。
** (Protocol.UndefinedError) protocol Enumerable not implemented for %REnum.StreamTest.HaltAcc{acc: 1..3} of type REnum.StreamTest.HaltAcc (a struct). This protocol is implemented for the following type(s): Date.Range, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, List, Map, MapSet, Range, Stream
動かないコード
# test/r_enum/stream_test.ecs
defmodule REnum.StreamTest do
use ExUnit.Case, async: true
doctest REnum.Stream.Native
defmodule HaltAcc do # test module内でstructの定義
defstruct [:acc]
defimpl Enumerable do
def count(_lazy), do: {:error, __MODULE__}
def member?(_lazy, _value), do: {:error, __MODULE__}
def slice(_lazy), do: {:error, __MODULE__}
def reduce(lazy, _acc, _fun) do
{:halted, Enum.to_list(lazy.acc)}
end
end
end
test "zip_with/2" do
zip_fun = &List.to_tuple/1
stream = %HaltAcc{acc: 1..3}
# ここのtestで落ちる
assert REnum.Stream.zip_with([1..3, stream], zip_fun) |> Enum.to_list() == [
{1, 1},
{2, 2},
{3, 3}
]
end
end
unit test内でstructを入れ子にして定義しています。
elixirのリポジトリ内でEarthlyを用いてテストを動かした場合は成功するのですが、コピーしてきたリポジトリ内でmix testを行っても上手く行きません。
※ Elixirのコード内でのtestの動かし方に関しては、また別の機会に調査してみようと思います。きっとmix testのコマンドではないのかもしれないです。
動く実装
iexで試したり、testの他ファイルやlib以下でtestに使うstructを定義してみたところ、lib以下に定義した場合に、正しい成功の挙動を確認することが出来ました。
それらを少し整理した実装が以下コードです。
# mix.exs
def project do
[
...
elixirc_paths: elixirc_paths(Mix.env()) # Compile対象をMIX_ENVによって変更する
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# test/support/halt_acc.ex
defmodule HaltAcc do # elixirc_pathsで定義したpathに含まれる箇所でstruct定義を行う
defstruct [:acc]
defimpl Enumerable do
def count(_lazy), do: {:error, __MODULE__}
def member?(_lazy, _value), do: {:error, __MODULE__}
def slice(_lazy), do: {:error, __MODULE__}
def reduce(lazy, _acc, _fun) do
{:halted, Enum.to_list(lazy.acc)}
end
end
end
# test/r_enum/stream_test.ecs
defmodule REnum.StreamTest do
use ExUnit.Case, async: true
doctest REnum.Stream.Native
test "zip_with/2" do
zip_fun = &List.to_tuple/1
stream = %HaltAcc{acc: 1..3}
# 成功!!
assert REnum.Stream.zip_with([1..3, stream], zip_fun) |> Enum.to_list() == [
{1, 1},
{2, 2},
{3, 3}
]
end
end
- test/support内に該当のstructを定義する
- compile対象のコードを、MIX_ENV == testの場合のみ["lib", "test/support"]とする
※ elixirc_pathsの対象はdefaultだと"lib"以下のみです。(多分きっとelixir_compile_pathの略だと思ってます)
まとめ
こちらの記事でもありましたが、Elixirではちょいちょいcompileのタイミングを考慮する必要があるのかもしれません。
ここの辺りをもう少し調査していけば、Elixirの内部実装に関しても、もっと詳しくなれそうです。
(ライブラリの作成が終わったら是非チャレンジしてみたいと思います。)
もし、こちらの事象と解決策に関して、ズバリ何が起きているのか、ご存知の方がおりましたら、コメントか何かでご教授頂けますと幸いです。