Current section

Files

Jump to
espec lib espec doc_test.ex
Raw

lib/espec/doc_test.ex

defmodule ESpec.DocTest do
@moduledoc """
Implements functionality similar to ExUnit.DocTest.
Provides doctest macro.
Read the ExUnit.DocTest documentation for more info.
There are three types of specs:
:test - examples where input and output can be evaluated:
iex> Enum.map [1, 2, 3], fn(x) ->
...> x * 2
...> end
[2,4,6]
Such examples will be converted to: 'expect(input).to eq(output)' assertion.
:inspect - examples which return complex structure so Elixir prints it as #Name<...>.
iex> Enum.into([a: 10, b: 20], HashDict.new)
#HashDict<[b: 20, a: 10]>
The examples will be converted to: 'expect(inspect input).to eq(output)'.
:error - examples with exceptions:
iex(1)> String.to_atom((fn() -> 1 end).())
** (ArgumentError) argument error
The examples will be converted to: expect(fn -> input end).to raise_exception(error_module, error_message).
"""
@doc "Parses the module and builds 'specs'."
defmacro doctest(module, opts \\ []) do
do_import = Keyword.get(opts, :import, false)
quote do
ESpec.DocTest.__do_doctest__(unquote(module), unquote(opts), unquote(do_import))
end
end
@doc false
defmacro __do_doctest__(module, opts, true) do
quote do
import unquote(module)
ESpec.DocTest.__create_doc_examples__(unquote(module), unquote(opts))
end
end
defmacro __do_doctest__(module, opts, false) do
quote do
ESpec.DocTest.__create_doc_examples__(unquote(module), unquote(opts))
end
end
@doc false
@lint false
defmacro __create_doc_examples__(module, opts) do
quote do
examples = ESpec.DocExample.extract(unquote(module))
examples = if Keyword.get(unquote(opts), :only, :false) do
ESpec.DocTest.__filter_only__(examples, unquote(opts)[:only])
else
examples
end
examples = if Keyword.get(unquote(opts), :except, false) do
ESpec.DocTest.__filter_except__(examples, unquote(opts)[:except])
else
examples
end
Enum.with_index(examples)
|> Enum.reduce({[], {nil, nil}}, fn({ex, index}, {prev_binding, {binding_fun, binding_arity}}) ->
context = Enum.reverse(@context)
{fun, arity} = ex.fun_arity
description = "Doctest for #{unquote(module)}.#{fun}/#{arity} (#{index})"
function = :"#{ESpec.Support.word_chars(description)}_#{index}"
@examples %ESpec.Example{description: description, module: __MODULE__, function: function,
opts: [], file: __ENV__.file, line: __ENV__.line, context: context,
shared: false}
binding = case {binding_fun, binding_arity} do
{^fun, ^arity} -> prev_binding
_ -> []
end
{string_to_eval, new_binding} =
cond do
ex.type == :test ->
{lhs, new_binding} = Code.eval_string(ex.lhs, binding, __ENV__)
{rhs, _} = Code.eval_string(ex.rhs, binding, __ENV__)
str = """
def #{function}(shared) do
shared[:key]
expect(#{inspect lhs}).to eq(#{inspect rhs})
end
"""
{str, new_binding}
ex.type == :error ->
{error_module, error_message} = ex.rhs
lhs = ex.lhs
str = """
def #{function}(shared) do
shared[:key]
expect(fn -> Code.eval_string(#{lhs}) end).to raise_exception(#{error_module}, "#{error_message}")
end
"""
{str, binding}
ex.type == :inspect ->
{lhs, new_binding} = Code.eval_string(ex.lhs, binding, __ENV__)
{rhs, _} = Code.eval_string(ex.rhs, binding, __ENV__)
lhs = inspect(lhs)
str = """
def #{function}(shared) do
shared[:key]
expect(#{inspect lhs}).to eq(#{inspect rhs})
end
"""
{str, new_binding}
true ->
raise RuntimeError, message: "Wrong %ESpec.DocExample{} type!"
end
Code.eval_string(string_to_eval, [], __ENV__)
{binding ++ new_binding, {fun, arity}}
end)
end
end
@doc false
def __filter_only__(examples, list) do
Enum.filter(examples, &Enum.member?(list, &1.fun_arity))
end
@doc false
def __filter_except__(examples, list) do
Enum.filter(examples, fn(ex) ->
!Enum.member?(list, ex.fun_arity)
end)
end
end