Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions lib/ex_unit/lib/ex_unit/doc_test.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule ExUnit.DocTest do
@moduledoc """
@moduledoc ~S"""
Extract test cases from the documentation.

Doctests allow us to generate tests from code examples found
Expand Down Expand Up @@ -131,7 +131,7 @@ defmodule ExUnit.DocTest do

You can also showcase expressions raising an exception, for example:

iex(1)> raise "some error"
iex> raise "some error"
** (RuntimeError) some error

Doctest will look for a line starting with `** (` and it will parse it
Expand All @@ -141,6 +141,19 @@ defmodule ExUnit.DocTest do
Therefore, it is possible to match on multiline messages as long as there
are no empty lines on the message itself.

Asserting on the full exception message might not be possible because it is
non-deterministic, or it might result in brittle tests if the exact message
changes and gets more detailed.
Since Elixir 1.19.0, doctests allow the use of an ellipsis (`...`) at the
end of messages:

iex> raise "some error in pid: #{inspect(self())}"
** (RuntimeError) some error in pid: ...

iex> raise "some error in pid:\n#{inspect(self())}"
** (RuntimeError) some error in pid:
...

## When not to use doctest

In general, doctests are not recommended when your code examples contain
Expand Down Expand Up @@ -565,7 +578,7 @@ defmodule ExUnit.DocTest do
"Doctest failed: expected exception #{inspect(exception)} but got " <>
"#{inspect(actual_exception)} with message #{inspect(actual_message)}"

actual_message != message ->
not error_message_matches?(actual_message, message) ->
"Doctest failed: wrong message for #{inspect(actual_exception)}\n" <>
"expected:\n" <>
" #{inspect(message)}\n" <>
Expand All @@ -588,6 +601,17 @@ defmodule ExUnit.DocTest do
end
end

defp error_message_matches?(actual, expected) when actual == expected, do: true

defp error_message_matches?(actual, expected) do
if String.ends_with?(expected, "...") do
ellipsis_removed = binary_slice(expected, 0..-4//1)
String.starts_with?(actual, ellipsis_removed)
else
false
end
end

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

Expand Down
30 changes: 30 additions & 0 deletions lib/ex_unit/test/ex_unit/doc_test_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,34 @@ defmodule ExUnit.DocTestTest.PatternMatching do
end
|> ExUnit.BeamHelpers.write_beam()

defmodule ExUnit.DocTestTest.Ellipsis do
@doc """
iex> ExUnit.DocTestTest.Ellipsis.same_line_err(self())
** (ArgumentError) Unexpected: ...
"""
def same_line_err(arg) do
raise ArgumentError, message: "Unexpected: #{inspect(arg)}"
end

@doc """
iex> ExUnit.DocTestTest.Ellipsis.multi_line_err(self())
** (ArgumentError) Unexpected:
...
"""
def multi_line_err(arg) do
raise ArgumentError, message: "Unexpected:\n#{inspect(arg)}"
end

@doc """
iex> ExUnit.DocTestTest.Ellipsis.triple_dot_err(:foo)
** (ArgumentError) :foo ...
"""
def triple_dot_err(arg) do
raise ArgumentError, message: "#{inspect(arg)} ..."
end
end
|> ExUnit.BeamHelpers.write_beam()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a test when the error message itself has "..." at the end?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 468bfbc.
I'm not sure it's too useful since technically if we allow for anything then there's little reason for ... not to pass, but it doesn't cost much to have it so I'm fine either way.


defmodule ExUnit.DocTestTest do
use ExUnit.Case

Expand All @@ -574,6 +602,8 @@ defmodule ExUnit.DocTestTest do
doctest ExUnit.DocTestTest.HaikuIndent4UsingInspectOpts,
inspect_opts: [custom_options: [indent: 4]]

doctest ExUnit.DocTestTest.Ellipsis

import ExUnit.CaptureIO

test "multiple functions filtered with :only" do
Expand Down
Loading