Skip to content

Commit 5aa5268

Browse files
committed
Implement extended doctest sessions. Closes #1020
1 parent 619f345 commit 5aa5268

File tree

2 files changed

+199
-89
lines changed

2 files changed

+199
-89
lines changed

lib/ex_unit/lib/ex_unit/doc_test.ex

Lines changed: 152 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,14 @@ defmodule ExUnit.DocTest do
7878
side effects. For example, if a doctest prints to standard output, doctest
7979
will not try to capture the output.
8080
81-
Similarly, doctest does not run in any kind of side box. So any module
81+
Similarly, doctest does not run in any kind of sandbox. So any module
8282
defined in a code example is going to linger throughout the whole test suite
8383
run.
8484
"""
8585

86-
defrecord Test, fun_arity: nil, line: nil, expr: nil, expected: nil
86+
defexception Error, message: nil
87+
88+
defrecord Test, fun_arity: nil, line: nil, exprs: []
8789

8890
@doc """
8991
This macro is used to generate ExUnit test cases for doctests.
@@ -151,87 +153,124 @@ defmodule ExUnit.DocTest do
151153
:"test doc at #{inspect m}.#{f}/#{a} (#{n})"
152154
end
153155

154-
defp test_content(Test[expected: { :test, expected }] = test, module, do_import) do
155-
line = test.line
156-
file = module.__info__(:compile)[:source]
156+
defp test_content(Test[exprs: exprs, line: line], module, do_import) do
157+
#IO.puts "Testing tests:"
158+
#Enum.each exprs, fn { expr, expected } ->
159+
#IO.puts "test '#{expr}' with expectation #{inspect expected}"
160+
#end
161+
#IO.puts ""
162+
163+
file = module.__info__(:compile)[:source]
157164
location = [line: line, file: Path.relative_to(file, System.cwd!)]
158165
stack = Macro.escape [{ module, :__MODULE__, 0, location }]
159166

160-
expr_ast = string_to_ast(module, line, file, test.expr)
161-
expected_ast = string_to_ast(module, line, file, expected)
167+
exc_filter_fn = (function do
168+
{ _, {:error, _, _} } -> true
169+
_ -> false
170+
end)
162171

163-
quote do
164-
unquote_splicing(test_import(module, do_import))
172+
exceptions_num = Enum.count exprs, exc_filter_fn
173+
if exceptions_num > 1 do
174+
# FIXME: stacktrace pointing to the doctest?
175+
raise Error, message: "Multiple exceptions in one doctest case are not supported"
176+
end
177+
178+
{ tests, whole_expr } = Enum.map_reduce exprs, "", fn {expr, expected}, acc ->
179+
{ test_case_content(expr, expected, module, line, file, stack), acc <> expr <> "\n" }
180+
end
181+
exception_expr = Enum.find(exprs, exc_filter_fn)
182+
183+
if nil?(exception_expr) do
184+
quote do
185+
unquote_splicing(test_import(module, do_import))
186+
try do
187+
# Put all tests into one context
188+
unquote_splicing(tests)
189+
rescue
190+
e in [ExUnit.ExpectationError] ->
191+
raise e, [], unquote(stack)
192+
actual ->
193+
raise ExUnit.ExpectationError,
194+
[ prelude: "Expected doctest",
195+
description: unquote(whole_expr),
196+
expected: "without an exception",
197+
reason: "complete",
198+
actual: inspect(actual) ],
199+
unquote(stack)
200+
end
201+
end
202+
else
203+
{ expr, {:error, exception, message} } = exception_expr
204+
quote do
205+
unquote_splicing(test_import(module, do_import))
206+
try do
207+
# Put all tests into one context
208+
unquote_splicing(tests)
209+
rescue
210+
e in [ExUnit.ExpectationError] ->
211+
case e.reason do
212+
"evaluate to" ->
213+
raise e, [], unquote(stack)
214+
"raise" ->
215+
raise(e)
216+
end
217+
218+
error in [unquote(exception)] ->
219+
unless error.message == unquote(message) do
220+
raise ExUnit.ExpectationError,
221+
[ prelude: "Expected doctest",
222+
description: unquote(expr),
223+
expected: "#{inspect unquote(exception)} with message #{inspect unquote(message)}",
224+
reason: "raise",
225+
actual: inspect(error) ],
226+
unquote(stack)
227+
end
165228

166-
try do
167-
v = unquote(expected_ast)
168-
case unquote(expr_ast) do
169-
^v -> :ok
170229
actual ->
171230
raise ExUnit.ExpectationError,
172231
[ prelude: "Expected doctest",
173-
description: unquote(test.expr),
174-
expected: inspect(v),
175-
reason: "evaluate to",
232+
description: unquote(whole_expr),
233+
expected: "#{inspect unquote(exception)}",
234+
reason: "complete or raise",
176235
actual: inspect(actual) ],
177236
unquote(stack)
178237
end
179-
rescue
180-
e in [ExUnit.ExpectationError] ->
181-
raise e, [], unquote(stack)
238+
end
239+
end
240+
end
241+
242+
defp test_case_content(expr, { :test, expected }, module, line, file, stack) do
243+
expr_ast = string_to_ast(module, line, file, expr)
244+
expected_ast = string_to_ast(module, line, file, expected)
245+
246+
quote do
247+
v = unquote(expected_ast)
248+
case unquote(expr_ast) do
249+
^v -> :ok
182250
actual ->
183251
raise ExUnit.ExpectationError,
184252
[ prelude: "Expected doctest",
185-
description: unquote(test.expr),
186-
expected: "without an exception",
187-
reason: "complete",
253+
description: unquote(expr),
254+
expected: inspect(v),
255+
reason: "evaluate to",
188256
actual: inspect(actual) ],
189257
unquote(stack)
190258
end
191259
end
192260
end
193261

194-
defp test_content(Test[expected: { :error, exception, message }] = test, module, do_import) do
195-
line = test.line
196-
file = module.__info__(:compile)[:source]
197-
location = [line: line, file: Path.relative_to(file, System.cwd!)]
198-
stack = Macro.escape [{ module, :__MODULE__, 0, location }]
199-
200-
expr_ast = string_to_ast(module, line, file, test.expr)
262+
defp test_case_content(expr, { :error, exception, _ }, module, line, file, stack) do
263+
expr_ast = string_to_ast(module, line, file, expr)
201264

202265
quote do
203-
unquote_splicing(test_import(module, do_import))
204-
205-
try do
206-
v = unquote(expr_ast)
207-
raise ExUnit.ExpectationError,
208-
[ prelude: "Expected doctest",
209-
description: unquote(test.expr),
210-
expected: "#{inspect unquote(exception)}[]",
211-
reason: "raise",
212-
actual: inspect(v) ],
213-
unquote(stack)
214-
rescue
215-
e in [ExUnit.ExpectationError] -> raise(e)
216-
error in [unquote(exception)] ->
217-
unless error.message == unquote(message) do
218-
raise ExUnit.ExpectationError,
219-
[ prelude: "Expected doctest",
220-
description: unquote(test.expr),
221-
expected: "#{inspect unquote(exception)} with message #{inspect unquote(message)}",
222-
reason: "raise",
223-
actual: inspect(error) ],
224-
unquote(stack)
225-
end
226-
error ->
227-
raise ExUnit.ExpectationError,
228-
[ prelude: "Expected doctest",
229-
description: unquote(test.expr),
230-
expected: "#{inspect unquote(exception)}",
231-
reason: "raise",
232-
actual: inspect(error) ],
233-
unquote(stack)
234-
end
266+
v = unquote(expr_ast)
267+
raise ExUnit.ExpectationError,
268+
[ prelude: "Expected doctest",
269+
description: unquote(expr),
270+
expected: "#{inspect unquote(exception)}[]",
271+
reason: "raise",
272+
actual: inspect(v) ],
273+
unquote(stack)
235274
end
236275
end
237276

@@ -286,57 +325,81 @@ defmodule ExUnit.DocTest do
286325

287326
defp extract_tests(line, doc) do
288327
lines = String.split(doc, %r/\n/) |> Enum.map(function(String.strip/1))
289-
extract_tests(lines, line, "", "", [])
328+
extract_tests(lines, line, "", "", [], true)
290329
end
291330

292-
defp extract_tests([], _line, "", "", acc), do: Enum.reverse(acc)
331+
defp extract_tests([], _line, "", "", [], _) do
332+
[]
333+
end
334+
335+
defp extract_tests([], _line, "", "", [test=Test[exprs: exprs]|t], _) do
336+
test = test.exprs(Enum.reverse(exprs))
337+
Enum.reverse([test|t])
338+
end
293339

294-
defp extract_tests([], line, expr_acc, expected_acc, acc) do
295-
test = Test[expr: expr_acc, line: line, expected: { :test, expected_acc }]
296-
Enum.reverse([test|acc])
340+
# End of input and we've still got a test pending.
341+
defp extract_tests([], _, expr_acc, expected_acc, [test=Test[exprs: exprs]|t], _) do
342+
test = test.exprs(Enum.reverse([{ expr_acc, {:test, expected_acc} } | exprs]))
343+
Enum.reverse([test|t])
297344
end
298345

299-
defp extract_tests([<< "iex>", _ :: binary>>|_] = list, line, expr_acc, expected_acc, acc) when expr_acc != "" and expected_acc != "" do
300-
test = Test[expr: expr_acc, line: line, expected: { :test, expected_acc }]
301-
extract_tests(list, line, "", "", [test|acc])
346+
# We've encountered the next test on an adjacent line. Put them into one group.
347+
defp extract_tests([<< "iex>", _ :: binary>>|_] = list, line, expr_acc, expected_acc, [test=Test[exprs: exprs]|t], newtest) when expr_acc != "" and expected_acc != "" do
348+
test = test.exprs([{ expr_acc, {:test, expected_acc} } | exprs])
349+
extract_tests(list, line, "", "", [test|t], newtest)
302350
end
303351

304-
defp extract_tests([<< "iex>", string :: binary>>|lines], line, expr_acc, expected_acc, acc) when expr_acc == "" do
305-
extract_tests(lines, line, string, expected_acc, acc)
352+
# Store expr_acc.
353+
defp extract_tests([<< "iex>", string :: binary>>|lines], line, "", expected_acc, acc, newtest) do
354+
if newtest do
355+
if match?([test=Test[exprs: exprs] | t], acc) do
356+
acc = [test.exprs(Enum.reverse(exprs)) | t]
357+
end
358+
acc = [Test[line: line]|acc]
359+
end
360+
extract_tests(lines, line, string, expected_acc, acc, false)
306361
end
307362

308-
defp extract_tests([<< "iex>", string :: binary>>|lines], line, expr_acc, expected_acc, acc) do
309-
extract_tests(lines, line, expr_acc <> "\n" <> string, expected_acc, acc)
363+
# Still gathering expr_acc. Synonym for the next clause.
364+
defp extract_tests([<< "iex>", string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) do
365+
extract_tests(lines, line, expr_acc <> "\n" <> string, expected_acc, acc, newtest)
310366
end
311367

312-
defp extract_tests([<< "...>", string :: binary>>|lines], line, expr_acc, expected_acc, acc) when expr_acc != "" do
313-
extract_tests(lines, line, expr_acc <> "\n" <> string, expected_acc, acc)
368+
# Still gathering expr_acc. Synonym for the previous clause.
369+
defp extract_tests([<< "...>", string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) when expr_acc != "" do
370+
extract_tests(lines, line, expr_acc <> "\n" <> string, expected_acc, acc, newtest)
314371
end
315372

316-
defp extract_tests([<< "iex(", _ :: 8, string :: binary>>|lines], line, expr_acc, expected_acc, acc) do
317-
extract_tests(["iex" <> skip_iex_number(string)|lines], line, expr_acc, expected_acc, acc)
373+
# Expression numbers are simply skipped.
374+
defp extract_tests([<< "iex(", _ :: 8, string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) do
375+
extract_tests(["iex" <> skip_iex_number(string)|lines], line, expr_acc, expected_acc, acc, newtest)
318376
end
319377

320-
defp extract_tests([<< "...(", _ :: 8, string :: binary>>|lines], line, expr_acc, expected_acc, acc) do
321-
extract_tests(["..." <> skip_iex_number(string)|lines], line, expr_acc, expected_acc, acc)
378+
# Expression numbers are simply skipped redux.
379+
defp extract_tests([<< "...(", _ :: 8, string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) do
380+
extract_tests(["..." <> skip_iex_number(string)|lines], line, expr_acc, expected_acc, acc, newtest)
322381
end
323382

324-
defp extract_tests([_|lines], line, "", "", acc) do
325-
extract_tests(lines, line, "", "", acc)
383+
# Skip empty or documentation line.
384+
defp extract_tests([_|lines], line, "", "", acc, _) do
385+
extract_tests(lines, line, "", "", acc, true)
326386
end
327387

328-
defp extract_tests([""|lines], line, expr_acc, expected_acc, acc) do
329-
test = Test[expr: expr_acc, line: line, expected: { :test, expected_acc }]
330-
extract_tests(lines, line, "", "", [test|acc])
388+
# Encountered an empty line, store pending test
389+
defp extract_tests([""|lines], line, expr_acc, expected_acc, [test=Test[exprs: exprs]|t], _) do
390+
test = test.exprs([{ expr_acc, {:test, expected_acc} } | exprs])
391+
extract_tests(lines, line, "", "", [test|t], true)
331392
end
332393

333-
defp extract_tests([<< "** (", string :: binary >>|lines], line, expr_acc, "", acc) do
334-
test = Test[expr: expr_acc, line: line, expected: extract_error(string, "")]
335-
extract_tests(lines, line, "", "", [test|acc])
394+
# Exception test.
395+
defp extract_tests([<< "** (", string :: binary >>|lines], line, expr_acc, "", [test=Test[exprs: exprs]|t], newtest) do
396+
test = test.exprs([{ expr_acc, extract_error(string, "") } | exprs])
397+
extract_tests(lines, line, "", "", [test|t], newtest)
336398
end
337399

338-
defp extract_tests([expected|lines], line, expr_acc, expected_acc, acc) do
339-
extract_tests(lines, line, expr_acc, expected_acc <> "\n" <> expected, acc)
400+
# Finally, parse expected_acc.
401+
defp extract_tests([expected|lines], line, expr_acc, expected_acc, acc, newtest) do
402+
extract_tests(lines, line, expr_acc, expected_acc <> "\n" <> expected, acc, newtest)
340403
end
341404

342405
defp extract_error(<< ")", t :: binary >>, acc) do

lib/ex_unit/test/ex_unit/doc_test_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,52 @@ defmodule ExUnit.DocTestTest.GoodModule do
99
"""
1010
def test_fun, do: 1
1111

12+
@doc """
13+
iex> a = 1
14+
iex> b = a + 2
15+
3
16+
iex> a + b
17+
4
18+
"""
19+
def single_context
20+
21+
@doc """
22+
iex> 1 + (fn() -> "" end).()
23+
** (ArithmeticError) bad argument in arithmetic expression
24+
25+
iex> 2 + (fn() -> :a end).()
26+
** (ArithmeticError) bad argument in arithmetic expression
27+
"""
28+
def two_exceptions
29+
1230
@doc """
1331
iex> 1 + (fn() -> :a end).()
1432
** (ArithmeticError) bad argument in arithmetic expression
1533
"""
1634
def exception_test, do: 1
1735
end
1836

37+
defmodule ExUnit.DocTestTest.ExceptionModule do
38+
@doc """
39+
iex> 1 + ""
40+
** (ArithmeticError) bad argument in arithmetic expression
41+
iex> 2 + ""
42+
** (ArithmeticError) bad argument in arithmetic expression
43+
"""
44+
def two_exceptions_in_single_context
45+
end
46+
47+
defmodule ExUnit.DocTestTest.LeakCheckModule do
48+
@doc """
49+
iex> a = 1
50+
1
51+
52+
iex> a + 1
53+
2
54+
"""
55+
def no_leak
56+
end
57+
1958
defmodule ExUnit.DocTestTest.SomewhatGoodModule do
2059
@doc """
2160
iex> test_fun
@@ -67,4 +106,12 @@ defmodule ExUnit.DocTestTest do
67106
doctest ExUnit.DocTestTest.SomewhatGoodModule, only: [test_fun: 0], import: true
68107
doctest ExUnit.DocTestTest.SomewhatGoodModule1, except: [test_fun1: 0], import: true
69108
doctest ExUnit.DocTestTest.NoImport
109+
110+
assert_raise ExUnit.DocTest.Error, "Multiple exceptions in one doctest case are not supported", fn ->
111+
doctest ExUnit.DocTestTest.ExceptionModule
112+
end
113+
114+
# FIXME: is it possible to test this?
115+
# ** (CompileError) .../doc_test_test.exs:55: function a/0 undefined
116+
#doctest ExUnit.DocTestTest.LeakCheckModule
70117
end

0 commit comments

Comments
 (0)