Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ellipsis in doctest exceptions #14233

Merged
merged 4 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 25 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,15 @@ defmodule ExUnit.DocTest do
end
end

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

defp error_message_matches?(actual, expected) do
case String.replace_suffix(expected, "...", "") do
^expected -> false
Copy link
Member

Choose a reason for hiding this comment

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

This seems to be an expensive check to verify if the string has ... or not. My suggestion is to use String.ends_with? and then binary_slice.

ellipsis_removed -> String.starts_with?(actual, ellipsis_removed)
end
end

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

Expand Down
22 changes: 22 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,26 @@ 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
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
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 +594,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