1Code.require_file("../../test_helper.exs", __DIR__)
2
3defmodule Mix.Tasks.TestTest do
4  use MixTest.Case
5
6  describe "ex_unit_opts/1" do
7    test "returns ex unit options" do
8      assert ex_unit_opts_from_given(unknown: "ok", seed: 13) == [seed: 13]
9    end
10
11    test "returns includes and excludes" do
12      included = [include: [:focus, key: "val"]]
13      assert ex_unit_opts_from_given(include: "focus", include: "key:val") == included
14
15      excluded = [exclude: [:focus, key: "val"]]
16      assert ex_unit_opts_from_given(exclude: "focus", exclude: "key:val") == excluded
17    end
18
19    test "translates :only into includes and excludes" do
20      assert ex_unit_opts_from_given(only: "focus") == [include: [:focus], exclude: [:test]]
21
22      only = [include: [:focus, :special], exclude: [:test]]
23      assert ex_unit_opts_from_given(only: "focus", include: "special") == only
24    end
25
26    test "translates :color into list containing an enabled key-value pair" do
27      assert ex_unit_opts_from_given(color: false) == [colors: [enabled: false]]
28      assert ex_unit_opts_from_given(color: true) == [colors: [enabled: true]]
29    end
30
31    test "translates :formatter into list of modules" do
32      assert ex_unit_opts_from_given(formatter: "A.B") == [formatters: [A.B]]
33    end
34
35    test "accepts custom :exit_status" do
36      assert {:exit_status, 5} in ex_unit_opts(exit_status: 5)
37    end
38
39    test "includes some default options" do
40      assert ex_unit_opts([]) == [
41               autorun: false,
42               exit_status: 2,
43               failures_manifest_file:
44                 Path.join(Mix.Project.manifest_path(), ".mix_test_failures")
45             ]
46    end
47
48    defp ex_unit_opts(opts) do
49      {ex_unit_opts, _allowed_files} = Mix.Tasks.Test.process_ex_unit_opts(opts)
50      ex_unit_opts
51    end
52
53    defp ex_unit_opts_from_given(passed) do
54      passed
55      |> ex_unit_opts()
56      |> Keyword.drop([:failures_manifest_file, :autorun, :exit_status])
57    end
58  end
59
60  describe "--stale" do
61    test "runs all tests for first run, then none on second" do
62      in_fixture("test_stale", fn ->
63        assert_stale_run_output("2 tests, 0 failures")
64
65        assert_stale_run_output("""
66        No stale tests
67        """)
68      end)
69    end
70
71    test "runs tests that depend on modified modules" do
72      in_fixture("test_stale", fn ->
73        assert_stale_run_output("2 tests, 0 failures")
74
75        set_all_mtimes()
76        force_recompilation("lib/b.ex")
77
78        assert_stale_run_output("1 test, 0 failures")
79
80        set_all_mtimes()
81        force_recompilation("lib/a.ex")
82
83        assert_stale_run_output("2 tests, 0 failures")
84      end)
85    end
86
87    test "doesn't write manifest when there are failures" do
88      in_fixture("test_stale", fn ->
89        assert_stale_run_output("2 tests, 0 failures")
90
91        set_all_mtimes()
92
93        File.write!("lib/b.ex", """
94        defmodule B do
95          def f, do: :error
96        end
97        """)
98
99        assert_stale_run_output("1 test, 1 failure")
100
101        assert_stale_run_output("1 test, 1 failure")
102      end)
103    end
104
105    test "runs tests that have changed" do
106      in_fixture("test_stale", fn ->
107        assert_stale_run_output("2 tests, 0 failures")
108
109        set_all_mtimes()
110        File.touch!("test/a_test_stale.exs")
111
112        assert_stale_run_output("1 test, 0 failures")
113      end)
114    end
115
116    test "runs tests that have changed test_helpers" do
117      in_fixture("test_stale", fn ->
118        assert_stale_run_output("2 tests, 0 failures")
119
120        set_all_mtimes()
121        File.touch!("test/test_helper.exs")
122
123        assert_stale_run_output("2 tests, 0 failures")
124      end)
125    end
126
127    test "runs all tests no matter what with --force" do
128      in_fixture("test_stale", fn ->
129        assert_stale_run_output("2 tests, 0 failures")
130
131        assert_stale_run_output(~w[--force], "2 tests, 0 failures")
132      end)
133    end
134  end
135
136  describe "--cover" do
137    test "reports the coverage of each app's modules in an umbrella" do
138      in_fixture("umbrella_test", fn ->
139        # This fixture by default results in coverage above the default threshold
140        # which should result in an exit status of 0.
141        assert {output, 0} = mix_code(["test", "--cover"])
142        assert output =~ "4 tests, 0 failures"
143
144        # For bar, we do regular --cover and also test protocols
145        assert output =~ """
146               Generating cover results ...
147
148               Percentage | Module
149               -----------|--------------------------
150                  100.00% | Bar.Protocol
151                  100.00% | Bar.Protocol.BitString
152               -----------|--------------------------
153                  100.00% | Total
154               """
155
156        assert output =~ "1 test, 0 failures"
157
158        # For foo, we do regular --cover and test it does not include bar
159        assert output =~ """
160               Generating cover results ...
161
162               Percentage | Module
163               -----------|--------------------------
164                  100.00% | Foo
165               -----------|--------------------------
166                  100.00% | Total
167               """
168
169        # We skip a test in bar to force coverage below the default threshold
170        # which should result in an exit status of 1.
171        assert {_, code} = mix_code(["test", "--cover", "--exclude", "maybe_skip"])
172
173        unless windows?() do
174          assert code == 3
175        end
176      end)
177    end
178
179    test "supports unified reports by using test.coverage" do
180      in_fixture("umbrella_test", fn ->
181        assert mix(["test", "--export-coverage", "default", "--cover"]) =~
182                 "Run \"mix test.coverage\" once all exports complete"
183
184        assert mix(["test.coverage"]) =~ """
185               Importing cover results: apps/bar/cover/default.coverdata
186               Importing cover results: apps/foo/cover/default.coverdata
187
188               Percentage | Module
189               -----------|--------------------------
190                  100.00% | Bar
191                  100.00% | Bar.Ignore
192                  100.00% | Bar.Protocol
193                  100.00% | Bar.Protocol.BitString
194                  100.00% | Foo
195               -----------|--------------------------
196                  100.00% | Total
197               """
198      end)
199    end
200  end
201
202  describe "--failed" do
203    test "loads only files with failures and runs just the failures" do
204      in_fixture("test_failed", fn ->
205        loading_only_passing_test_msg = "loading OnlyPassingTest"
206
207        # Run `mix test` once to record failures...
208        output = mix(["test"])
209        assert output =~ loading_only_passing_test_msg
210        assert output =~ "4 tests, 2 failures"
211
212        # `mix test --failed` runs only failed tests and avoids loading files with no failures
213        output = mix(["test", "--failed"])
214        refute output =~ loading_only_passing_test_msg
215        assert output =~ "2 tests, 2 failures"
216
217        # `mix test --failed` can be applied to a directory or file
218        output = mix(["test", "test/passing_and_failing_test_failed.exs", "--failed"])
219        assert output =~ "1 test, 1 failure"
220
221        # `--failed` composes with an `--only` filter by running the intersection.
222        # Of the failing tests, 1 is tagged with `@tag :foo`.
223        # Of the passing tests, 1 is tagged with `@tag :foo`.
224        # But only the failing test with that tag should run.
225        output = mix(["test", "--failed", "--only", "foo"])
226        assert output =~ "2 tests, 1 failure, 1 excluded"
227
228        # Run again to give it a chance to record as passed
229        System.put_env("PASS_FAILING_TESTS", "true")
230        assert mix(["test", "--failed"]) =~ "2 tests, 0 failures"
231
232        # Nothing should get run if we try it again since everything is passing.
233        assert mix(["test", "--failed"]) =~ "There are no tests to run"
234
235        # `--failed` and `--stale` cannot be combined
236        output = mix(["test", "--failed", "--stale"])
237        assert output =~ "Combining --failed and --stale is not supported"
238      end)
239    after
240      System.delete_env("PASS_FAILING_TESTS")
241    end
242  end
243
244  describe "--listen-on-stdin" do
245    test "runs tests after input" do
246      in_fixture("test_stale", fn ->
247        port = mix_port(~w[test --stale --listen-on-stdin])
248
249        assert receive_until_match(port, "seed", "") =~ "2 tests"
250
251        Port.command(port, "\n")
252
253        assert receive_until_match(port, "No stale tests", "") =~ "Restarting..."
254      end)
255    end
256
257    test "does not exit on compilation failure" do
258      in_fixture("test_stale", fn ->
259        File.write!("lib/b.ex", """
260        defmodule B do
261          def f, do: error_not_a_var
262        end
263        """)
264
265        port = mix_port(~w[test --listen-on-stdin])
266
267        assert receive_until_match(port, "error", "") =~ "lib/b.ex"
268
269        File.write!("lib/b.ex", """
270        defmodule B do
271          def f, do: A.f
272        end
273        """)
274
275        Port.command(port, "\n")
276
277        assert receive_until_match(port, "seed", "") =~ "2 tests"
278
279        File.write!("test/b_test_stale.exs", """
280        defmodule BTest do
281          use ExUnit.Case
282
283          test "f" do
284            assert B.f() == error_not_a_var
285          end
286        end
287        """)
288
289        Port.command(port, "\n")
290
291        message = "undefined function error_not_a_var"
292        assert receive_until_match(port, message, "") =~ "test/b_test_stale.exs"
293
294        File.write!("test/b_test_stale.exs", """
295        defmodule BTest do
296          use ExUnit.Case
297
298          test "f" do
299            assert B.f() == :ok
300          end
301        end
302        """)
303
304        Port.command(port, "\n")
305
306        assert receive_until_match(port, "seed", "") =~ "2 tests"
307      end)
308    end
309  end
310
311  describe "--partitions" do
312    test "splits tests into partitions" do
313      in_fixture("test_stale", fn ->
314        assert mix(["test", "--partitions", "3", "--cover"], [{"MIX_TEST_PARTITION", "1"}]) =~
315                 "1 test, 0 failures"
316
317        assert mix(["test", "--partitions", "3", "--cover"], [{"MIX_TEST_PARTITION", "2"}]) =~
318                 "1 test, 0 failures"
319
320        assert mix(["test", "--partitions", "3", "--cover"], [{"MIX_TEST_PARTITION", "3"}]) =~
321                 "There are no tests to run"
322
323        assert File.regular?("cover/1.coverdata")
324        assert File.regular?("cover/2.coverdata")
325        refute File.regular?("cover/3.coverdata")
326
327        assert mix(["test.coverage"]) == """
328               Importing cover results: cover/1.coverdata
329               Importing cover results: cover/2.coverdata
330
331               Percentage | Module
332               -----------|--------------------------
333                  100.00% | A
334                  100.00% | B
335               -----------|--------------------------
336                  100.00% | Total
337
338               Generated HTML coverage results in \"cover\" directory
339               """
340      end)
341    end
342
343    test "raises when no partition is given even with Mix.shell() change" do
344      in_fixture("test_stale", fn ->
345        File.write!("test/test_helper.exs", """
346        Mix.shell(Mix.Shell.Process)
347        ExUnit.start()
348        """)
349
350        assert_run_output(
351          ["--partitions", "4"],
352          "The MIX_TEST_PARTITION environment variable must be set"
353        )
354      end)
355    end
356
357    test "do not raise if partitions flag is set to 1 and no partition given" do
358      in_fixture("test_stale", fn ->
359        assert mix(["test", "--partitions", "1"], []) =~
360                 "2 tests, 0 failures"
361
362        assert mix(["test", "--partitions", "1"], [{"MIX_TEST_PARTITION", ""}]) =~
363                 "2 tests, 0 failures"
364
365        assert mix(["test", "--partitions", "1"], [{"MIX_TEST_PARTITION", "1"}]) =~
366                 "2 tests, 0 failures"
367      end)
368    end
369
370    test "raise if partitions is set to non-positive value" do
371      in_fixture("test_stale", fn ->
372        File.write!("test/test_helper.exs", """
373        Mix.shell(Mix.Shell.Process)
374        ExUnit.start()
375        """)
376
377        assert_run_output(
378          ["--partitions", "0"],
379          "--partitions : expected to be positive integer, got 0"
380        )
381
382        assert_run_output(
383          ["--partitions", "-1"],
384          "--partitions : expected to be positive integer, got -1"
385        )
386      end)
387    end
388  end
389
390  describe "logs and errors" do
391    test "logs test absence for a project with no test paths" do
392      in_fixture("test_stale", fn ->
393        File.rm_rf!("test")
394
395        assert_run_output("There are no tests to run")
396      end)
397    end
398
399    test "raises when no test runs even with Mix.shell() change" do
400      in_fixture("test_stale", fn ->
401        File.write!("test/test_helper.exs", """
402        Mix.shell(Mix.Shell.Process)
403        ExUnit.start()
404        """)
405
406        assert_run_output(
407          ["--only", "unknown"],
408          "The --only option was given to \"mix test\" but no test was executed"
409        )
410      end)
411    end
412
413    test "raises an exception if line numbers are given with multiple files" do
414      in_fixture("test_stale", fn ->
415        assert_run_output(
416          [
417            "test/a_test_stale.exs",
418            "test/b_test_stale.exs:4"
419          ],
420          "Line numbers can only be used when running a single test file"
421        )
422      end)
423    end
424
425    test "umbrella with file path" do
426      in_fixture("umbrella_test", fn ->
427        # Run false positive test first so at least the code is compiled
428        # and we can perform more aggressive assertions later
429        assert mix(["test", "apps/unknown_app/test"]) =~ """
430               ==> bar
431               Paths given to "mix test" did not match any directory/file: apps/unknown_app/test
432               ==> foo
433               Paths given to "mix test" did not match any directory/file: apps/unknown_app/test
434               """
435
436        output = mix(["test", "apps/bar/test/bar_tests.exs"])
437
438        assert output =~ """
439               ==> bar
440               ....
441               """
442
443        refute output =~ "==> foo"
444        refute output =~ "Paths given to \"mix test\" did not match any directory/file"
445
446        output = mix(["test", "apps/bar/test/bar_tests.exs:10"])
447
448        assert output =~ """
449               ==> bar
450               Excluding tags: [:test]
451               Including tags: [line: \"10\"]
452
453               .
454               """
455
456        refute output =~ "==> foo"
457        refute output =~ "Paths given to \"mix test\" did not match any directory/file"
458      end)
459    end
460  end
461
462  describe "--warnings-as-errors" do
463    test "fail on warning in tests" do
464      in_fixture("test_stale", fn ->
465        msg =
466          "Test suite aborted after successful execution due to warnings while using the --warnings-as-errors option"
467
468        refute mix(["test", "--warnings-as-errors"]) =~ msg
469
470        File.write!("lib/warning.ex", """
471        unused_compile_var = 1
472        """)
473
474        File.write!("test/warning_test_stale.exs", """
475        defmodule WarningTest do
476          use ExUnit.Case
477
478          test "warning" do
479            unused_test_var = 1
480          end
481        end
482        """)
483
484        output = mix(["test", "--warnings-as-errors", "test/warning_test_stale.exs"])
485        assert output =~ "variable \"unused_compile_var\" is unused"
486        assert output =~ "variable \"unused_test_var\" is unused"
487        assert output =~ msg
488      end)
489    end
490
491    test "mark failed tests" do
492      in_fixture("test_failed", fn ->
493        File.write!("test/warning_test_failed.exs", """
494        defmodule WarningTest do
495          use ExUnit.Case
496
497          test "warning" do
498            unused_var = 123
499          end
500        end
501        """)
502
503        output = mix(["test", "--warnings-as-errors"])
504        assert output =~ "2 failures"
505        refute output =~ "Test suite aborted after successful execution"
506        output = mix(["test", "--failed"])
507        assert output =~ "2 failures"
508      end)
509    end
510  end
511
512  describe "--exit-status" do
513    @describetag :unix
514
515    test "returns custom exit status" do
516      in_fixture("test_failed", fn ->
517        {output, exit_status} = mix_code(["test", "--exit-status", "5"])
518        assert output =~ "2 failures"
519        assert exit_status == 5
520      end)
521    end
522  end
523
524  defp receive_until_match(port, expected, acc) do
525    receive do
526      {^port, {:data, output}} ->
527        acc = acc <> output
528
529        if output =~ expected do
530          acc
531        else
532          receive_until_match(port, expected, acc)
533        end
534    end
535  end
536
537  defp set_all_mtimes(time \\ {{2010, 1, 1}, {0, 0, 0}}) do
538    Enum.each(Path.wildcard("**", match_dot: true), &File.touch!(&1, time))
539  end
540
541  defp assert_stale_run_output(opts \\ [], expected) do
542    assert_run_output(["--stale" | opts], expected)
543  end
544
545  defp assert_run_output(opts \\ [], expected) do
546    assert mix(["test" | opts]) =~ expected
547  end
548end
549