Skip to content

Commit e6c052d

Browse files
committed
Refactor doctests for upcoming bug fixes
1 parent 52b6dd7 commit e6c052d

File tree

2 files changed

+98
-192
lines changed

2 files changed

+98
-192
lines changed

lib/ex_unit/lib/ex_unit/doc_test.ex

Lines changed: 95 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,9 @@ defmodule ExUnit.DocTest do
261261
file = Path.relative_to_cwd(file)
262262
tags = [doctest: file] ++ Keyword.get(opts, :tags, [])
263263

264-
extract_tests(1, doc, module)
265-
|> Stream.map(&normalize_test(&1, :moduledoc))
266-
|> Stream.with_index(1)
267-
|> Enum.map(fn {test, acc} ->
268-
tags = [doctest_line: test.line] ++ tags
269-
{"#{file} (#{acc})", test_content(test, module, false, file), tags}
264+
extract_tests(1, doc, module, :moduledoc)
265+
|> Enum.with_index(fn test, acc ->
266+
{"#{file} (#{acc + 1})", test_content(test, module, false, file), test_tags(test, tags)}
270267
end)
271268
end
272269

@@ -336,8 +333,8 @@ defmodule ExUnit.DocTest do
336333
## Compilation of extracted tests
337334

338335
defp compile_test(test, module, do_import, n, file, tags) do
339-
tags = [doctest_line: test.line] ++ tags
340-
{test_name(test, module, n), test_content(test, module, do_import, file), tags}
336+
{test_name(test, module, n), test_content(test, module, do_import, file),
337+
test_tags(test, tags)}
341338
end
342339

343340
defp test_name(%{fun_arity: :moduledoc}, m, n) do
@@ -369,6 +366,10 @@ defmodule ExUnit.DocTest do
369366
{:__block__, [], test_import(module, do_import) ++ tests}
370367
end
371368

369+
defp test_tags(test, tags) do
370+
[doctest_line: test.line] ++ tags
371+
end
372+
372373
defp multiple_exceptions?(exprs) do
373374
Enum.count(exprs, fn
374375
{_, {:error, _, _}, _} -> true
@@ -575,9 +576,7 @@ defmodule ExUnit.DocTest do
575576
do: "The documentation chunk in the module is invalid"
576577

577578
defp extract_from_moduledoc(annotation, %{"en" => doc}, module) do
578-
for test <- extract_tests(:erl_anno.line(annotation), doc, module) do
579-
normalize_test(test, :moduledoc)
580-
end
579+
extract_tests(:erl_anno.line(annotation), doc, module, :moduledoc)
581580
end
582581

583582
defp extract_from_moduledoc(_, _doc, _module), do: []
@@ -588,10 +587,7 @@ defmodule ExUnit.DocTest do
588587

589588
defp extract_from_doc({{_, name, arity}, annotation, _, %{"en" => doc}, _}, module) do
590589
line = :erl_anno.line(annotation)
591-
592-
for test <- extract_tests(line, doc, module) do
593-
normalize_test(test, {name, arity})
594-
end
590+
extract_tests(line, doc, module, {name, arity})
595591
end
596592

597593
defp extract_from_doc(_doc, _module),
@@ -648,7 +644,7 @@ defmodule ExUnit.DocTest do
648644
"""
649645
end
650646

651-
adjusted_lines = [{stripped_line, line_no} | adjusted_lines]
647+
adjusted_lines = [{adjust_prompt(stripped_line, line_no, module), line_no} | adjusted_lines]
652648

653649
next =
654650
cond do
@@ -694,210 +690,118 @@ defmodule ExUnit.DocTest do
694690
end
695691
end
696692

697-
@fences ["```", "~~~"]
693+
defp adjust_prompt("iex(" <> rest = line, line_no, module),
694+
do: "iex>" <> skip_iex_number(rest, line_no, module, line)
698695

699-
defp extract_tests(line_no, doc, module) do
700-
all_lines = String.split(doc, ["\r\n", "\n"], trim: false)
701-
lines = adjust_indent(all_lines, line_no + 1, module)
702-
extract_tests(lines, [], [], [], true, module, [])
703-
end
696+
defp adjust_prompt("...(" <> rest = line, line_no, module),
697+
do: "...>" <> skip_iex_number(rest, line_no, module, line)
704698

705-
defp extract_tests(lines, expr_acc, expected_acc, acc, new_test, module, formatted)
699+
defp adjust_prompt(line, _line_no, _module),
700+
do: line
706701

707-
defp extract_tests([], [], [], [], _, _, _) do
708-
[]
709-
end
702+
defp skip_iex_number(string, line_no, module, line) do
703+
case :binary.split(string, ")>") do
704+
[_pre, post] ->
705+
post
710706

711-
defp extract_tests([], [], [], acc, _, _, _) do
712-
Enum.reverse(acc)
713-
end
707+
[_] ->
708+
message =
709+
"unknown IEx prompt: #{inspect(line)}.\nAccepted formats are: iex>, iex(1)>, ...>, ...(1)>}"
714710

715-
# End of input and we've still got a test pending.
716-
defp extract_tests([], expr_acc, expected_acc, [test | rest], _, _, formatted) do
717-
test = add_expr(test, expr_acc, expected_acc, formatted)
718-
Enum.reverse([test | rest])
711+
raise Error, line: line_no, module: module, message: message
712+
end
719713
end
720714

721-
# We've encountered the next test on an adjacent line. Put them into one group.
722-
defp extract_tests(
723-
[{"iex>" <> _, _} | _] = list,
724-
expr_acc,
725-
expected_acc,
726-
[test | rest],
727-
new_test,
728-
module,
729-
formatted
730-
)
731-
when expr_acc != [] and expected_acc != [] do
732-
test = add_expr(test, expr_acc, expected_acc, formatted)
733-
extract_tests(list, [], [], [test | rest], new_test, module, [])
734-
end
735-
736-
# Store expr_acc and start a new test case.
737-
defp extract_tests(
738-
[{"iex>" <> string = line, line_no} | lines],
739-
[],
740-
expected_acc,
741-
acc,
742-
true,
743-
module,
744-
_
745-
) do
746-
test = %{line: line_no, fun_arity: nil, exprs: []}
747-
extract_tests(lines, [string], expected_acc, [test | acc], false, module, line)
715+
defp chunk_tests(lines, acc) do
716+
case lines
717+
|> Enum.drop_while(&(not test_started?(&1)))
718+
|> Enum.split_while(&(not test_finished?(&1))) do
719+
{[], []} -> Enum.reverse(acc)
720+
{test, []} -> Enum.reverse([test | acc])
721+
{[], [_empty_line | lines]} -> chunk_tests(lines, acc)
722+
{test, [_empty_line | lines]} -> chunk_tests(lines, [test | acc])
723+
end
748724
end
749725

750-
# Store expr_acc.
751-
defp extract_tests(
752-
[{"iex>" <> string = line, _} | lines],
753-
[],
754-
expected_acc,
755-
acc,
756-
false,
757-
module,
758-
_
759-
) do
760-
extract_tests(lines, [string], expected_acc, acc, false, module, line)
761-
end
726+
defp test_started?({"iex>" <> _, _}), do: true
727+
defp test_started?(_), do: false
762728

763-
# Still gathering expr_acc. Synonym for the next clause.
764-
defp extract_tests(
765-
[{"iex>" <> string = line, _} | lines],
766-
expr_acc,
767-
expected_acc,
768-
acc,
769-
new_test,
770-
module,
771-
formatted
772-
) do
773-
expr_acc = add_line(expr_acc, string)
774-
formatted = add_line(formatted, line)
775-
extract_tests(lines, expr_acc, expected_acc, acc, new_test, module, formatted)
729+
defp test_finished?({line, _}) do
730+
case line do
731+
"" -> true
732+
"```" <> _ -> true
733+
"~~~" <> _ -> true
734+
_ -> false
735+
end
776736
end
777737

778-
# Still gathering expr_acc. Synonym for the previous clause.
779-
defp extract_tests(
780-
[{"...>" <> string = line, _} | lines],
781-
expr_acc,
782-
expected_acc,
783-
acc,
784-
new_test,
785-
module,
786-
formatted
787-
)
788-
when expr_acc != [] do
789-
expr_acc = add_line(expr_acc, string)
790-
formatted = add_line(formatted, line)
791-
extract_tests(lines, expr_acc, expected_acc, acc, new_test, module, formatted)
792-
end
793-
794-
# Expression numbers are simply skipped.
795-
defp extract_tests(
796-
[{<<"iex(", _>> <> string = line, line_no} | lines],
797-
expr_acc,
798-
expected_acc,
799-
acc,
800-
new_test,
801-
module,
802-
formatted
803-
) do
804-
new_line = {"iex" <> skip_iex_number(string, module, line_no, line), line_no}
805-
extract_tests([new_line | lines], expr_acc, expected_acc, acc, new_test, module, formatted)
806-
end
807-
808-
# Expression numbers are simply skipped redux.
809-
defp extract_tests(
810-
[{<<"...(", _>> <> string, line_no} = line | lines],
811-
expr_acc,
812-
expected_acc,
813-
acc,
814-
new_test,
815-
module,
816-
formatted
817-
) do
818-
new_line = {"..." <> skip_iex_number(string, module, line_no, line), line_no}
819-
extract_tests([new_line | lines], expr_acc, expected_acc, acc, new_test, module, formatted)
820-
end
821-
822-
# Skip empty or documentation line.
823-
defp extract_tests([_ | lines], [], [], acc, _, module, _formatted) do
824-
extract_tests(lines, [], [], acc, true, module, [])
825-
end
826-
827-
# Encountered end of fenced code block, store pending test
828-
defp extract_tests(
829-
[{<<fence::3-bytes>> <> _, _} | lines],
830-
expr_acc,
831-
expected_acc,
832-
[test | rest],
833-
_new_test,
834-
module,
835-
formatted
836-
)
837-
when fence in @fences and expr_acc != [] do
838-
test = add_expr(test, expr_acc, expected_acc, formatted)
839-
extract_tests(lines, [], [], [test | rest], true, module, [])
840-
end
841-
842-
# Encountered an empty line, store pending test
843-
defp extract_tests(
844-
[{"", _} | lines],
845-
expr_acc,
846-
expected_acc,
847-
[test | rest],
848-
_new_test,
849-
module,
850-
formatted
851-
) do
852-
test = add_expr(test, expr_acc, expected_acc, formatted)
853-
extract_tests(lines, [], [], [test | rest], true, module, [])
738+
defp extract_tests(line_no, doc, module, fun_arity) do
739+
doc
740+
|> String.split(["\r\n", "\n"], trim: false)
741+
|> adjust_indent(line_no + 1, module)
742+
|> chunk_tests([])
743+
|> Enum.map(&build_test(&1, fun_arity))
854744
end
855745

856-
# Finally, parse expected_acc.
857-
defp extract_tests([{expected, _} | lines], expr_acc, [], acc, new_test, module, formatted) do
858-
extract_tests(lines, expr_acc, expected, acc, new_test, module, formatted)
746+
defp build_test([{_, line_no} | _] = lines, fun_arity) do
747+
exprs = build_test(lines, [], [], [], [])
748+
%{line: line_no, exprs: Enum.reverse(exprs), fun_arity: fun_arity}
859749
end
860750

861-
defp extract_tests(
862-
[{expected, _} | lines],
863-
expr_acc,
864-
expected_acc,
865-
acc,
866-
new_test,
867-
module,
868-
formatted
869-
) do
870-
expected_acc = add_line(expected_acc, expected)
871-
extract_tests(lines, expr_acc, expected_acc, acc, new_test, module, formatted)
751+
defp build_test([], [_ | _] = expr, expected, formatted, acc) do
752+
add_expr(acc, expr, expected, formatted)
872753
end
873754

874-
defp add_line(acc, line) do
875-
[acc, [?\n, line]]
755+
# Tidy up the previous expression before starting a new one.
756+
defp build_test(
757+
[{"iex>" <> _, _} | _] = list,
758+
[_ | _] = expr,
759+
[_ | _] = expected,
760+
formatted,
761+
acc
762+
) do
763+
acc = add_expr(acc, expr, expected, formatted)
764+
build_test(list, [], [], [], acc)
876765
end
877766

878-
defp skip_iex_number(")>" <> string, _module, _line_no, _line) do
879-
">" <> string
767+
# We start a new expression.
768+
defp build_test(
769+
[{"iex>" <> string = line, _} | lines],
770+
expr,
771+
expected,
772+
formatted,
773+
acc
774+
) do
775+
expr = add_line(expr, string)
776+
formatted = add_line(formatted, line)
777+
build_test(lines, expr, expected, formatted, acc)
880778
end
881779

882-
defp skip_iex_number("", module, line_no, line) do
883-
message =
884-
"unknown IEx prompt: #{inspect(line)}.\nAccepted formats are: iex>, iex(1)>, ...>, ...(1)>}"
885-
886-
raise Error, line: line_no, module: module, message: message
780+
# Continuation of an expression.
781+
defp build_test(
782+
[{"...>" <> string = line, _} | lines],
783+
expr,
784+
expected,
785+
formatted,
786+
acc
787+
) do
788+
expr = add_line(expr, string)
789+
formatted = add_line(formatted, line)
790+
build_test(lines, expr, expected, formatted, acc)
887791
end
888792

889-
defp skip_iex_number(<<_>> <> string, module, line_no, line) do
890-
skip_iex_number(string, module, line_no, line)
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)
891796
end
892797

893-
defp normalize_test(%{exprs: exprs} = test, fa) do
894-
%{test | fun_arity: fa, exprs: Enum.reverse(exprs)}
895-
end
798+
defp add_line([], line), do: [line]
799+
defp add_line(acc, line), do: [acc, [?\n, line]]
896800

897-
defp add_expr(%{exprs: exprs} = test, expr_lines, expected_lines, formatted_lines) do
801+
defp add_expr(exprs, expr_lines, expected_lines, formatted_lines) do
898802
expected = IO.iodata_to_binary(expected_lines)
899803
doctest = IO.iodata_to_binary([?\n, formatted_lines, ?\n, expected])
900-
%{test | exprs: [{expr_lines, tag_expected(expected), doctest} | exprs]}
804+
[{expr_lines, tag_expected(expected), doctest} | exprs]
901805
end
902806

903807
defp tag_expected(expected) do

lib/ex_unit/test/ex_unit/doc_test_test.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,9 @@ defmodule ExUnit.DocTestTest.PatternMatching do
456456
iex> %{b: _, d: :e} = %{a: :c, d: :e}
457457
458458
iex> %{year: 2001, day: 1} = ~D[2000-01-01]
459+
"""
459460

461+
@doc """
460462
iex> adder = fn int -> int + 1 end
461463
iex> num =
462464
...> adder.(0)
@@ -468,7 +470,7 @@ defmodule ExUnit.DocTestTest.PatternMatching do
468470
# false assertions do not accidentally raise
469471
iex> false = (List.flatten([]) != [])
470472
"""
471-
def doctest(), do: :ok
473+
def passing(), do: :ok
472474
end
473475
|> ExUnit.BeamHelpers.write_beam()
474476

0 commit comments

Comments
 (0)