1Code.require_file("../test_helper.exs", __DIR__)
2
3defmodule ExUnit.FormatterTest do
4  use ExUnit.Case
5
6  import ExUnit.Formatter
7  doctest ExUnit.Formatter
8
9  defmacrop catch_assertion(expr) do
10    quote do
11      try do
12        unquote(expr)
13      rescue
14        ex -> ex
15      end
16    end
17  end
18
19  defp test_module do
20    %ExUnit.TestModule{name: Hello}
21  end
22
23  defp test do
24    %ExUnit.Test{name: :world, module: Hello, tags: %{file: __ENV__.file, line: 1}}
25  end
26
27  def falsy() do
28    false
29  end
30
31  defp formatter(_key, value), do: value
32
33  defp diff_formatter(:diff_enabled?, _default), do: true
34  defp diff_formatter(_key, value), do: value
35
36  test "formats test case filters" do
37    filters = [run: true, slow: false]
38    assert format_filters(filters, :exclude) =~ "Excluding tags: [run: true, slow: false]"
39    assert format_filters(filters, :include) =~ "Including tags: [run: true, slow: false]"
40  end
41
42  test "formats test errors" do
43    failure = [{:error, catch_error(raise "oops"), []}]
44
45    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
46             1) world (Hello)
47                test/ex_unit/formatter_test.exs:1
48                ** (RuntimeError) oops
49           """
50  end
51
52  test "formats test exits" do
53    failure = [{:exit, 1, []}]
54
55    assert format_test_failure(test(), failure, 1, 80, &formatter/2) == """
56             1) world (Hello)
57                test/ex_unit/formatter_test.exs:1
58                ** (exit) 1
59           """
60  end
61
62  test "formats test exits with mfa" do
63    failure = [{:exit, {:bye, {:mod, :fun, []}}, []}]
64
65    assert format_test_failure(test(), failure, 1, 80, &formatter/2) == """
66             1) world (Hello)
67                test/ex_unit/formatter_test.exs:1
68                ** (exit) exited in: :mod.fun()
69                    ** (EXIT) :bye
70           """
71  end
72
73  test "formats test exits with function clause mfa" do
74    {error, stack} =
75      try do
76        Access.fetch(:foo, :bar)
77      catch
78        :error, error -> {error, __STACKTRACE__}
79      end
80
81    failure = [{:exit, {{error, stack}, {:mod, :fun, []}}, []}]
82
83    assert trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2)) =~
84             """
85               1) world (Hello)
86                  test/ex_unit/formatter_test.exs:1
87                  ** (exit) exited in: :mod.fun()
88                     ** (EXIT) an exception was raised:
89                       ** (FunctionClauseError) no function clause matching in Access.fetch/2
90
91                       The following arguments were given to Access.fetch/2:
92
93                           # 1
94                           :foo
95
96                           # 2
97                           :bar
98
99                       Attempted function clauses (showing 5 out of 5):
100
101                           def fetch(%module{} = container, key)
102             """
103  end
104
105  test "formats test exits with assertion mfa" do
106    {error, stack} =
107      try do
108        assert 1 == 2
109      rescue
110        error -> {error, __STACKTRACE__}
111      end
112
113    failure = [{:exit, {{error, stack}, {:mod, :fun, []}}, []}]
114
115    assert trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2)) =~
116             """
117               1) world (Hello)
118                  test/ex_unit/formatter_test.exs:1
119                  ** (exit) exited in: :mod.fun()
120                     ** (EXIT) an exception was raised:
121                       Assertion with == failed
122                       code:  assert 1 == 2
123                       left:  1
124                       right: 2
125             """
126  end
127
128  test "formats test throws" do
129    failure = [{:throw, 1, []}]
130
131    assert format_test_failure(test(), failure, 1, 80, &formatter/2) == """
132             1) world (Hello)
133                test/ex_unit/formatter_test.exs:1
134                ** (throw) 1
135           """
136  end
137
138  test "formats test EXITs" do
139    failure = [{{:EXIT, self()}, 1, []}]
140
141    assert format_test_failure(test(), failure, 1, 80, &formatter/2) == """
142             1) world (Hello)
143                test/ex_unit/formatter_test.exs:1
144                ** (EXIT from #{inspect(self())}) 1
145           """
146  end
147
148  test "formats test EXITs with function clause errors" do
149    {error, stack} =
150      try do
151        Access.fetch(:foo, :bar)
152      catch
153        :error, error -> {error, __STACKTRACE__}
154      end
155
156    failure = [{{:EXIT, self()}, {error, stack}, []}]
157    format = trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2))
158
159    assert format =~
160             """
161               1) world (Hello)
162                  test/ex_unit/formatter_test.exs:1
163                  ** (EXIT from #{inspect(self())}) an exception was raised:
164
165                       ** (FunctionClauseError) no function clause matching in Access.fetch/2
166
167                       The following arguments were given to Access.fetch/2:
168
169                           # 1
170                           :foo
171
172                           # 2
173                           :bar
174
175                       Attempted function clauses (showing 5 out of 5):
176
177                           def fetch(%module{} = container, key)
178             """
179
180    assert format =~ ~r"lib/access.ex:\d+: Access.fetch/2"
181  end
182
183  test "formats test EXITs with assertion errors" do
184    {error, stack} =
185      try do
186        assert 1 == 2
187      rescue
188        error -> {error, __STACKTRACE__}
189      end
190
191    failure = [{{:EXIT, self()}, {error, stack}, []}]
192
193    assert trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2)) =~
194             """
195               1) world (Hello)
196                  test/ex_unit/formatter_test.exs:1
197                  ** (EXIT from #{inspect(self())}) an exception was raised:
198
199                       Assertion with == failed
200                       code:  assert 1 == 2
201                       left:  1
202                       right: 2
203             """
204  end
205
206  test "formats test errors with test_location_relative_path" do
207    Application.put_env(:ex_unit, :test_location_relative_path, "apps/sample")
208    failure = [{:error, catch_error(raise "oops"), []}]
209
210    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
211             1) world (Hello)
212                apps/sample/test/ex_unit/formatter_test.exs:1
213                ** (RuntimeError) oops
214           """
215  after
216    Application.delete_env(:ex_unit, :test_location_relative_path)
217  end
218
219  test "formats test errors with code snippets" do
220    stack = {Hello, :world, 1, [file: __ENV__.file, line: 3]}
221    failure = [{:error, catch_error(raise "oops"), [stack]}]
222
223    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
224             1) world (Hello)
225                test/ex_unit/formatter_test.exs:1
226                ** (RuntimeError) oops
227                code: defmodule ExUnit.FormatterTest do
228           """
229  end
230
231  test "formats stacktraces" do
232    stacktrace = [{Oops, :wrong, 1, [file: "formatter_test.exs", line: 1]}]
233    failure = [{:error, catch_error(raise "oops"), stacktrace}]
234
235    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
236             1) world (Hello)
237                test/ex_unit/formatter_test.exs:1
238                ** (RuntimeError) oops
239                stacktrace:
240                  formatter_test.exs:1: Oops.wrong/1
241           """
242  end
243
244  test "formats assertions" do
245    failure = [{:error, catch_assertion(assert ExUnit.FormatterTest.falsy()), []}]
246
247    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
248             1) world (Hello)
249                test/ex_unit/formatter_test.exs:1
250                Expected truthy, got false
251                code: assert ExUnit.FormatterTest.falsy()
252           """
253  end
254
255  test "formats assertions with patterns and values" do
256    failure = [{:error, catch_assertion(assert {1, 2, 3} > {1, 2, 3}), []}]
257
258    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
259             1) world (Hello)
260                test/ex_unit/formatter_test.exs:1
261                Assertion with > failed, both sides are exactly equal
262                code: assert {1, 2, 3} > {1, 2, 3}
263                left: {1, 2, 3}
264           """
265
266    failure = [{:error, catch_assertion(assert {3, 2, 1} = {1, 2, 3}), []}]
267
268    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
269             1) world (Hello)
270                test/ex_unit/formatter_test.exs:1
271                match (=) failed
272                code:  assert {3, 2, 1} = {1, 2, 3}
273                left:  {3, 2, 1}
274                right: {1, 2, 3}
275           """
276  end
277
278  nfc_hello = String.normalize("héllo", :nfc)
279  nfd_hello = String.normalize("héllo", :nfd)
280
281  test "formats assertions with hints" do
282    failure = [{:error, catch_assertion(assert unquote(nfc_hello) == unquote(nfd_hello)), []}]
283
284    assert format_test_failure(test(), failure, 1, 80, &diff_formatter/2) =~ """
285             1) world (Hello)
286                test/ex_unit/formatter_test.exs:1
287                Assertion with == failed
288                code:  assert "#{unquote(nfc_hello)}" == "#{unquote(nfd_hello)}"
289                left:  "#{unquote(nfc_hello)}"
290                right: "#{unquote(nfd_hello)}"
291                hint:  you are comparing strings that have the same visual representation but are made of different Unicode codepoints
292           """
293  end
294
295  test "formats multiple assertions" do
296    failure = [
297      {:error, catch_assertion(assert ExUnit.FormatterTest.falsy()), []},
298      {:error, catch_assertion(assert 1 == 2), []}
299    ]
300
301    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
302             1) world (Hello)
303                test/ex_unit/formatter_test.exs:1
304
305                Failure #1
306                Expected truthy, got false
307                code: assert ExUnit.FormatterTest.falsy()
308
309                Failure #2
310                Assertion with == failed
311                code:  assert 1 == 2
312                left:  1
313                right: 2
314           """
315  end
316
317  defp trim_multiline_whitespace(string) do
318    String.replace(string, ~r"\n\s+\n", "\n\n")
319  end
320
321  test "blames function clause error" do
322    {error, stack} =
323      try do
324        Access.fetch(:foo, :bar)
325      rescue
326        exception -> {exception, __STACKTRACE__}
327      end
328
329    failure = format_test_failure(test(), [{:error, error, [hd(stack)]}], 1, 80, &formatter/2)
330
331    assert trim_multiline_whitespace(failure) =~ """
332             1) world (Hello)
333                test/ex_unit/formatter_test.exs:1
334                ** (FunctionClauseError) no function clause matching in Access.fetch/2
335
336                The following arguments were given to Access.fetch/2:
337
338                    # 1
339                    :foo
340
341                    # 2
342                    :bar
343
344                Attempted function clauses (showing 5 out of 5):
345
346                    def fetch(%module{} = container, key)
347           """
348
349    assert failure =~ ~r"\(elixir #{System.version()}\) lib/access\.ex:\d+: Access\.fetch/2"
350  end
351
352  test "formats setup_all errors" do
353    failure = [{:error, catch_error(raise "oops"), []}]
354
355    assert format_test_all_failure(test_module(), failure, 1, 80, &formatter/2) =~ """
356             1) Hello: failure on setup_all callback, all tests have been invalidated
357                ** (RuntimeError) oops
358           """
359  end
360
361  test "formats matches correctly" do
362    failure = [{:error, catch_assertion(assert %{a: :b} = %{a: :c}), []}]
363
364    assert format_test_all_failure(test_module(), failure, 1, :infinity, &formatter/2) =~ """
365             1) Hello: failure on setup_all callback, all tests have been invalidated
366                match (=) failed
367                code:  assert %{a: :b} = %{a: :c}
368                left:  %{a: :b}
369                right: %{a: :c}
370           """
371  end
372
373  test "formats assertions with operators with no limit" do
374    failure = [{:error, catch_assertion(assert [1, 2, 3] == [4, 5, 6]), []}]
375
376    assert format_test_all_failure(test_module(), failure, 1, :infinity, &formatter/2) =~ """
377             1) Hello: failure on setup_all callback, all tests have been invalidated
378                Assertion with == failed
379                code:  assert [1, 2, 3] == [4, 5, 6]
380                left:  [1, 2, 3]
381                right: [4, 5, 6]
382           """
383  end
384
385  test "formats assertions with operators with column limit" do
386    failure = [{:error, catch_assertion(assert [1, 2, 3] == [4, 5, 6]), []}]
387
388    assert format_test_all_failure(test_module(), failure, 1, 15, &formatter/2) =~ """
389             1) Hello: failure on setup_all callback, all tests have been invalidated
390                Assertion with == failed
391                code:  assert [1, 2, 3] == [4, 5, 6]
392                left:  [1,
393                        2,
394                        3]
395                right: [4,
396                        5,
397                        6]
398           """
399  end
400
401  test "formats assertions with complex function call arguments" do
402    failure = [{:error, catch_assertion(assert is_list(List.to_tuple([1, 2, 3]))), []}]
403
404    assert format_test_all_failure(test_module(), failure, 1, 80, &formatter/2) =~ """
405             1) Hello: failure on setup_all callback, all tests have been invalidated
406                Expected truthy, got false
407                code: assert is_list(List.to_tuple([1, 2, 3]))
408                arguments:
409
410                    # 1
411                    {1, 2, 3}
412           """
413
414    failure = [{:error, catch_assertion(assert is_list({1, 2})), []}]
415
416    assert format_test_all_failure(test_module(), failure, 1, 80, &formatter/2) =~ """
417             1) Hello: failure on setup_all callback, all tests have been invalidated
418                Expected truthy, got false
419                code: assert is_list({1, 2})
420           """
421  end
422
423  test "formats assertions with message with multiple lines" do
424    message = "Some meaningful error:\nuseful info\nanother useful info"
425    failure = [{:error, catch_assertion(assert(false, message)), []}]
426
427    assert format_test_all_failure(test_module(), failure, 1, :infinity, &formatter/2) =~ """
428             1) Hello: failure on setup_all callback, all tests have been invalidated
429                Some meaningful error:
430                useful info
431                another useful info
432           """
433  end
434
435  defmodule BadInspect do
436    defstruct key: 0
437
438    defimpl Inspect do
439      def inspect(struct, opts) when is_atom(opts) do
440        struct.unknown
441      end
442    end
443  end
444
445  test "inspect failure" do
446    failure = [{:error, catch_assertion(assert :will_fail == %BadInspect{}), []}]
447
448    message =
449      "got FunctionClauseError with message \"no function clause matching " <>
450        "in Inspect.ExUnit.FormatterTest.BadInspect.inspect/2\" while inspecting " <>
451        "%{__struct__: ExUnit.FormatterTest.BadInspect, key: 0}"
452
453    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
454             1) world (Hello)
455                test/ex_unit/formatter_test.exs:1
456                Assertion with == failed
457                code:  assert :will_fail == %BadInspect{}
458                left:  :will_fail
459                right: %Inspect.Error{
460                         message: #{inspect(message)}
461                       }
462           """
463  end
464
465  defmodule BadMessage do
466    defexception key: 0
467
468    @impl true
469    def message(_message) do
470      raise "oops"
471    end
472  end
473
474  test "message failure" do
475    failure = [{:error, catch_error(raise BadMessage), []}]
476
477    message =
478      "got RuntimeError with message \"oops\" while retrieving Exception.message/1 " <>
479        "for %ExUnit.FormatterTest.BadMessage{key: 0}. Stacktrace:"
480
481    assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
482             1) world (Hello)
483                test/ex_unit/formatter_test.exs:1
484                ** (ExUnit.FormatterTest.BadMessage) #{message}
485           """
486  end
487end
488