Skip to content

Commit 491cf4f

Browse files
committed
Fix doctest line failure with multiple assertions
1 parent e6c052d commit 491cf4f

File tree

2 files changed

+125
-82
lines changed

2 files changed

+125
-82
lines changed

lib/ex_unit/lib/ex_unit/doc_test.ex

Lines changed: 97 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,6 @@ defmodule ExUnit.DocTest do
346346
end
347347

348348
defp test_content(%{exprs: exprs, line: line}, module, do_import, file) do
349-
location = [line: line, file: Path.relative_to_cwd(file)]
350-
stack = Macro.escape([{module, :__MODULE__, 0, location}])
351-
352349
if multiple_exceptions?(exprs) do
353350
raise Error,
354351
line: line,
@@ -358,11 +355,7 @@ defmodule ExUnit.DocTest do
358355
"please separate your iex> prompts by multiple newlines to start new examples"
359356
end
360357

361-
tests =
362-
Enum.map(exprs, fn {expr, expected, doctest} ->
363-
test_case_content(expr, expected, location, stack, doctest)
364-
end)
365-
358+
tests = Enum.map(exprs, fn expr -> test_case_content(expr, module, file) end)
366359
{:__block__, [], test_import(module, do_import) ++ tests}
367360
end
368361

@@ -372,66 +365,74 @@ defmodule ExUnit.DocTest do
372365

373366
defp multiple_exceptions?(exprs) do
374367
Enum.count(exprs, fn
375-
{_, {:error, _, _}, _} -> true
368+
%{expected: {:error, _, _}} -> true
376369
_ -> false
377370
end) > 1
378371
end
379372

380-
defp test_case_content(expr_lines, :test, location, stack, doctest) do
381-
string_to_quoted(location, stack, expr_lines, doctest) |> insert_assertions()
373+
defp test_case_content(%{expected: :test} = data, module, file) do
374+
%{expr: expr, expr_line: expr_line, doctest: doctest} = data
375+
string_to_quoted(module, file, expr_line, expr, doctest) |> insert_assertions()
382376
end
383377

384-
defp test_case_content(expr_lines, {:test, expected}, location, stack, doctest) do
385-
expr_ast = string_to_quoted(location, stack, expr_lines, doctest) |> insert_assertions()
386-
expected_ast = string_to_quoted(update_line(location, expr_lines), stack, expected, doctest)
378+
defp test_case_content(%{expected: {:test, expected}} = data, module, file) do
379+
%{expr: expr, expr_line: expr_line, expected_line: expected_line, doctest: doctest} = data
380+
expr_ast = string_to_quoted(module, file, expr_line, expr, doctest) |> insert_assertions()
381+
expected_ast = string_to_quoted(module, file, expected_line, expected, doctest)
387382
last_expr = Macro.to_string(last_expr(expr_ast))
388383

389384
quote do
390-
value = unquote(expr_ast)
391-
expected = unquote(expected_ast)
392-
doctest = unquote(doctest)
393-
last_expr = unquote(last_expr)
394-
expected_expr = unquote(expected)
395-
stack = unquote(stack)
396-
397-
ExUnit.DocTest.__test__(value, expected, doctest, last_expr, expected_expr, stack)
385+
ExUnit.DocTest.__test__(
386+
unquote(expr_ast),
387+
unquote(expected_ast),
388+
unquote(doctest),
389+
unquote(last_expr),
390+
unquote(expected),
391+
unquote(module),
392+
unquote(file),
393+
unquote(expr_line)
394+
)
398395
end
399396
end
400397

401-
defp test_case_content(expr_lines, {:inspect, expected}, location, stack, doctest) do
402-
expr_ast = string_to_quoted(location, stack, expr_lines, doctest) |> insert_assertions()
398+
defp test_case_content(%{expected: {:inspect, expected}} = data, module, file) do
399+
%{expr: expr, expr_line: expr_line, doctest: doctest} = data
400+
expr_ast = string_to_quoted(module, file, expr_line, expr, doctest) |> insert_assertions()
403401
last_expr = Macro.to_string(last_expr(expr_ast))
404402

405403
quote do
406-
value = unquote(expr_ast)
407-
expected = unquote(expected)
408-
doctest = unquote(doctest)
409-
last_expr = unquote(last_expr)
410-
expected_expr = unquote(inspect(expected))
411-
stack = unquote(stack)
412-
413-
ExUnit.DocTest.__inspect__(value, expected, doctest, last_expr, expected_expr, stack)
404+
ExUnit.DocTest.__inspect__(
405+
unquote(expr_ast),
406+
unquote(expected),
407+
unquote(doctest),
408+
unquote(last_expr),
409+
unquote(inspect(expected)),
410+
unquote(module),
411+
unquote(file),
412+
unquote(expr_line)
413+
)
414414
end
415415
end
416416

417-
defp test_case_content(expr, {:error, exception, message}, location, stack, doctest) do
418-
expr_ast = string_to_quoted(location, stack, expr, doctest)
417+
defp test_case_content(%{expected: {:error, exception, message}} = data, module, file) do
418+
%{expr: expr, expr_line: expr_line, doctest: doctest} = data
419+
expr_ast = string_to_quoted(module, file, expr_line, expr, doctest)
419420

420421
quote do
421-
stack = unquote(stack)
422-
message = unquote(message)
423-
doctest = unquote(doctest)
424-
exception = unquote(exception)
425-
ExUnit.DocTest.__error__(fn -> unquote(expr_ast) end, message, exception, doctest, stack)
422+
ExUnit.DocTest.__error__(
423+
fn -> unquote(expr_ast) end,
424+
unquote(message),
425+
unquote(exception),
426+
unquote(doctest),
427+
unquote(module),
428+
unquote(file),
429+
unquote(expr_line)
430+
)
426431
end
427432
end
428433

429-
defp update_line(location, lines) do
430-
Keyword.replace_lazy(location, :line, &(&1 + length(lines)))
431-
end
432-
433434
@doc false
434-
def __test__(value, expected, doctest, last_expr, expected_expr, stack) do
435+
def __test__(value, expected, doctest, last_expr, expected_expr, module, file, line) do
435436
case value do
436437
^expected ->
437438
{:ok, value}
@@ -445,12 +446,12 @@ defmodule ExUnit.DocTest do
445446
right: expected
446447
]
447448

448-
reraise ExUnit.AssertionError, error, stack
449+
reraise ExUnit.AssertionError, error, stack(module, file, line)
449450
end
450451
end
451452

452453
@doc false
453-
def __inspect__(value, expected, doctest, last_expr, expected_expr, parent_stack) do
454+
def __inspect__(value, expected, doctest, last_expr, expected_expr, module, file, line) do
454455
result =
455456
try do
456457
inspect(value, safe: false)
@@ -470,12 +471,12 @@ defmodule ExUnit.DocTest do
470471
{extra, stack} ->
471472
expr = "inspect(#{last_expr}) === #{String.trim(expected_expr)}"
472473
error = [doctest: doctest, expr: expr] ++ extra
473-
reraise ExUnit.AssertionError, error, stack ++ parent_stack
474+
reraise ExUnit.AssertionError, error, stack ++ stack(module, file, line)
474475
end
475476
end
476477

477478
@doc false
478-
def __error__(fun, message, exception, doctest, stack) do
479+
def __error__(fun, message, exception, doctest, module, file, line) do
479480
try do
480481
fun.()
481482
rescue
@@ -500,24 +501,24 @@ defmodule ExUnit.DocTest do
500501
end
501502

502503
if failed do
503-
reraise ExUnit.AssertionError, [message: failed, doctest: doctest], stack
504+
reraise ExUnit.AssertionError,
505+
[message: failed, doctest: doctest],
506+
stack(module, file, line)
504507
end
505508
else
506509
_ ->
507510
failed = "Doctest failed: expected exception #{inspect(exception)} but nothing was raised"
508511
error = [message: failed, doctest: doctest]
509-
reraise ExUnit.AssertionError, error, stack
512+
reraise ExUnit.AssertionError, error, stack(module, file, line)
510513
end
511514
end
512515

513516
defp test_import(_mod, false), do: []
514517
defp test_import(mod, _), do: [quote(do: import(unquote(mod)))]
515518

516-
defp string_to_quoted(location, stack, expr, doctest) do
517-
expr = IO.iodata_to_binary(expr)
518-
519+
defp string_to_quoted(module, file, line, expr, doctest) when is_binary(expr) do
519520
try do
520-
Code.string_to_quoted!(expr, location)
521+
Code.string_to_quoted!(expr, file: file, line: line)
521522
rescue
522523
e ->
523524
ex_message = "(#{inspect(e.__struct__)}) #{Exception.message(e)}"
@@ -544,11 +545,18 @@ defmodule ExUnit.DocTest do
544545
end
545546

546547
quote do
547-
reraise ExUnit.AssertionError, unquote(opts), unquote(stack)
548+
reraise ExUnit.AssertionError,
549+
unquote(opts),
550+
unquote(Macro.escape(stack(module, file, line)))
548551
end
549552
end
550553
end
551554

555+
defp stack(module, file, line) do
556+
location = [line: line, file: Path.relative_to_cwd(file)]
557+
[{module, :__MODULE__, 0, location}]
558+
end
559+
552560
## Extraction of the tests
553561

554562
defp extract(module) do
@@ -743,38 +751,36 @@ defmodule ExUnit.DocTest do
743751
|> Enum.map(&build_test(&1, fun_arity))
744752
end
745753

746-
defp build_test([{_, line_no} | _] = lines, fun_arity) do
747-
exprs = build_test(lines, [], [], [], [])
754+
defp build_test([{"iex>" <> string = line, line_no} | lines], fun_arity) do
755+
exprs = build_test(lines, [string], [], [line], [], line_no)
748756
%{line: line_no, exprs: Enum.reverse(exprs), fun_arity: fun_arity}
749757
end
750758

751-
defp build_test([], [_ | _] = expr, expected, formatted, acc) do
752-
add_expr(acc, expr, expected, formatted)
753-
end
754-
755-
# Tidy up the previous expression before starting a new one.
759+
# Started a new expression.
756760
defp build_test(
757-
[{"iex>" <> _, _} | _] = list,
761+
[{"iex>" <> _, new_line_no} | _] = list,
758762
[_ | _] = expr,
759763
[_ | _] = expected,
760764
formatted,
761-
acc
765+
acc,
766+
line_no
762767
) do
763-
acc = add_expr(acc, expr, expected, formatted)
764-
build_test(list, [], [], [], acc)
768+
acc = add_expr(acc, expr, expected, formatted, line_no)
769+
build_test(list, [], [], [], acc, new_line_no)
765770
end
766771

767-
# We start a new expression.
772+
# Continuation of an expression.
768773
defp build_test(
769774
[{"iex>" <> string = line, _} | lines],
770775
expr,
771776
expected,
772777
formatted,
773-
acc
778+
acc,
779+
line_no
774780
) do
775781
expr = add_line(expr, string)
776782
formatted = add_line(formatted, line)
777-
build_test(lines, expr, expected, formatted, acc)
783+
build_test(lines, expr, expected, formatted, acc, line_no)
778784
end
779785

780786
# Continuation of an expression.
@@ -783,25 +789,40 @@ defmodule ExUnit.DocTest do
783789
expr,
784790
expected,
785791
formatted,
786-
acc
792+
acc,
793+
line_no
787794
) do
788795
expr = add_line(expr, string)
789796
formatted = add_line(formatted, line)
790-
build_test(lines, expr, expected, formatted, acc)
797+
build_test(lines, expr, expected, formatted, acc, line_no)
791798
end
792799

793-
# Otherwise, it is expected lines.
794-
defp build_test([{line, _} | lines], expr, expected, formatted, acc) do
795-
build_test(lines, expr, add_line(expected, line), formatted, acc)
800+
# Expected lines.
801+
defp build_test([{line, _} | lines], expr, expected, formatted, acc, line_no) do
802+
build_test(lines, expr, add_line(expected, line), formatted, acc, line_no)
803+
end
804+
805+
# We are done.
806+
defp build_test([], [_ | _] = expr, expected, formatted, acc, line_no) do
807+
add_expr(acc, expr, expected, formatted, line_no)
796808
end
797809

798810
defp add_line([], line), do: [line]
799811
defp add_line(acc, line), do: [acc, [?\n, line]]
800812

801-
defp add_expr(exprs, expr_lines, expected_lines, formatted_lines) do
813+
defp add_expr(exprs, expr_lines, expected_lines, formatted_lines, line_no) do
802814
expected = IO.iodata_to_binary(expected_lines)
803815
doctest = IO.iodata_to_binary([?\n, formatted_lines, ?\n, expected])
804-
[{expr_lines, tag_expected(expected), doctest} | exprs]
816+
817+
expr = %{
818+
expr: IO.iodata_to_binary(expr_lines),
819+
expr_line: line_no,
820+
expected: tag_expected(expected),
821+
expected_line: line_no + length(expr_lines),
822+
doctest: doctest
823+
}
824+
825+
[expr | exprs]
805826
end
806827

807828
defp tag_expected(expected) do

lib/ex_unit/test/ex_unit/doc_test_test.exs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ defmodule ExUnit.DocTestTest.Invalid do
245245
:mixed
246246
"""
247247
def mixed, do: :ok
248+
249+
@doc """
250+
iex> 1 + 2
251+
3
252+
iex> 123 +
253+
:mixed
254+
"""
255+
def invalid_second, do: :ok
248256
end
249257
|> ExUnit.BeamHelpers.write_beam()
250258

@@ -704,7 +712,7 @@ defmodule ExUnit.DocTestTest do
704712
3
705713
`
706714
stacktrace:
707-
test/ex_unit/doc_test_test.exs:#{starting_line + 13}: ExUnit.DocTestTest.Invalid (module)
715+
test/ex_unit/doc_test_test.exs:#{starting_line + 14}: ExUnit.DocTestTest.Invalid (module)
708716
"""
709717

710718
assert output =~ """
@@ -719,7 +727,7 @@ defmodule ExUnit.DocTestTest do
719727
3
720728
```
721729
stacktrace:
722-
test/ex_unit/doc_test_test.exs:#{starting_line + 21}: ExUnit.DocTestTest.Invalid (module)
730+
test/ex_unit/doc_test_test.exs:#{starting_line + 22}: ExUnit.DocTestTest.Invalid (module)
723731
"""
724732

725733
assert output =~ """
@@ -734,7 +742,7 @@ defmodule ExUnit.DocTestTest do
734742
3
735743
```
736744
stacktrace:
737-
test/ex_unit/doc_test_test.exs:#{starting_line + 29}: ExUnit.DocTestTest.Invalid (module)
745+
test/ex_unit/doc_test_test.exs:#{starting_line + 30}: ExUnit.DocTestTest.Invalid (module)
738746
"""
739747

740748
assert output =~ """
@@ -757,7 +765,7 @@ defmodule ExUnit.DocTestTest do
757765
iex> {:ok, :oops}
758766
{:ok, #Inspect<[]>}
759767
stacktrace:
760-
test/ex_unit/doc_test_test.exs:#{starting_line + 42}: ExUnit.DocTestTest.Invalid (module)
768+
test/ex_unit/doc_test_test.exs:#{starting_line + 43}: ExUnit.DocTestTest.Invalid (module)
761769
"""
762770

763771
assert output =~ """
@@ -786,7 +794,7 @@ defmodule ExUnit.DocTestTest do
786794
iex> :bar
787795
1 + * 1
788796
stacktrace:
789-
test/ex_unit/doc_test_test.exs:#{starting_line + 54}: ExUnit.DocTestTest.Invalid (module)
797+
test/ex_unit/doc_test_test.exs:#{starting_line + 56}: ExUnit.DocTestTest.Invalid (module)
790798
"""
791799

792800
assert output =~ """
@@ -803,7 +811,21 @@ defmodule ExUnit.DocTestTest do
803811
test/ex_unit/doc_test_test.exs:#{starting_line + 61}: ExUnit.DocTestTest.Invalid (module)
804812
"""
805813

806-
assert output =~ "10 doctests, 10 failures"
814+
assert output =~ """
815+
11) doctest ExUnit.DocTestTest.Invalid.invalid_second/0 (11) (ExUnit.DocTestTest.InvalidCompiled)
816+
test/ex_unit/doc_test_test.exs:#{doctest_line}
817+
Doctest did not compile, got: (TokenMissingError) test/ex_unit/doc_test_test.exs:#{starting_line + 69}:6: syntax error: expression is incomplete
818+
#{line_placeholder(starting_line + 69)} |
819+
#{starting_line + 69} | 123 +
820+
#{line_placeholder(starting_line + 69)} | ^
821+
doctest:
822+
iex> 123 +
823+
:mixed
824+
stacktrace:
825+
test/ex_unit/doc_test_test.exs:#{starting_line + 69}: ExUnit.DocTestTest.Invalid (module)
826+
"""
827+
828+
assert output =~ "11 doctests, 11 failures"
807829
end
808830

809831
test "pattern matching assertions in doctests" do

0 commit comments

Comments
 (0)