diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex index 9eb55689afa..038205a30a0 100644 --- a/lib/ex_unit/lib/ex_unit.ex +++ b/lib/ex_unit/lib/ex_unit.ex @@ -73,16 +73,18 @@ defmodule ExUnit do * `:time` - the time to run the test * `:tags` - the test tags * `:logs` - the captured logs + * `:type` - the test type """ - defstruct [:name, :case, :state, time: 0, tags: %{}, logs: ""] + defstruct [:name, :case, :state, time: 0, tags: %{}, logs: "", type: :test] @type t :: %__MODULE__{ name: atom, case: module, state: ExUnit.state, time: non_neg_integer, - tags: map} + tags: map, + type: atom} end defmodule TestCase do @@ -211,6 +213,9 @@ defmodule ExUnit do on formatting and reporters (defaults to 20) * `:timeout` - set the timeout for the tests (default 60_000ms) + + * `:plural_rules` - See `ExUnit.plural_rule/1` and `ExUnit.plural_rule/2`. + You should not set this option directly. """ def configure(options) do Enum.each options, fn {k, v} -> @@ -225,6 +230,42 @@ defmodule ExUnit do Application.get_all_env(:ex_unit) end + @doc """ + Returns the pluralization for `word`. + + If one is not registered, returns `"\#{word}s"`. + """ + @spec plural_rule(binary) :: binary + def plural_rule(word) when not is_binary(word), + do: raise_plural_argument_error("word") + def plural_rule(word) do + configuration() + |> Keyword.get(:plural_rules, %{}) + |> Map.get(word, "#{word}s") + end + + @doc """ + Registers a `pluralization` for `word`. + + If one is already registered, it is replaced. + """ + @spec plural_rule(binary, binary) :: :ok + def plural_rule(word, _pluralization) when not is_binary(word), + do: raise_plural_argument_error("word") + def plural_rule(_word, pluralization) when not is_binary(pluralization), + do: raise_plural_argument_error("pluralization") + def plural_rule(word, pluralization) do + plural_rules = + configuration() + |> Keyword.get(:plural_rules, %{}) + |> Map.put(word, pluralization) + + configure(plural_rules: plural_rules) + end + + defp raise_plural_argument_error(argument_name), + do: raise ArgumentError, message: "`#{argument_name}` must be a binary" + @doc """ API used to run the tests. It is invoked automatically if ExUnit is started via `ExUnit.start/1`. diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex index ce5093a310d..4091221dd69 100644 --- a/lib/ex_unit/lib/ex_unit/case.ex +++ b/lib/ex_unit/lib/ex_unit/case.ex @@ -132,6 +132,9 @@ defmodule ExUnit.Case do * `:timeout` - customizes the test timeout in milliseconds (defaults to 60000) * `:report` - include the given tags and context keys on error reports, see the "Reporting tags" section + * `:type` - customizes the test's type in reports (defaults to `:test`). The + test type will be converted to a string and pluralized for display. You + can use `ExUnit.plural_rule/2` to set a custom pluralization. ### Reporting tags @@ -324,6 +327,14 @@ defmodule ExUnit.Case do |> Map.merge(%{line: line, file: file, registered: registered}) test = %ExUnit.Test{name: name, case: mod, tags: tags} + + test = + if tags[:type] do + %{test | type: tags[:type]} + else + test + end + Module.put_attribute(mod, :ex_unit_tests, test) Enum.each [:tag | registered_attributes], fn(attribute) -> @@ -363,6 +374,10 @@ defmodule ExUnit.Case do Map.has_key?(tags, tag) do raise "cannot set tag #{inspect tag} because it is reserved by ExUnit" end + + unless is_atom(tags[:type]), + do: raise "value for tag `:type` must be an atom" + tags end diff --git a/lib/ex_unit/lib/ex_unit/cli_formatter.ex b/lib/ex_unit/lib/ex_unit/cli_formatter.ex index 62e0059e500..c110170f78a 100644 --- a/lib/ex_unit/lib/ex_unit/cli_formatter.ex +++ b/lib/ex_unit/lib/ex_unit/cli_formatter.ex @@ -15,7 +15,7 @@ defmodule ExUnit.CLIFormatter do trace: opts[:trace], colors: Keyword.put_new(opts[:colors], :enabled, IO.ANSI.enabled?), width: get_terminal_width(), - tests_counter: 0, + tests_counter: %{}, failures_counter: 0, skipped_counter: 0, invalids_counter: 0 @@ -43,12 +43,12 @@ defmodule ExUnit.CLIFormatter do else IO.write success(".", config) end - {:ok, %{config | tests_counter: config.tests_counter + 1}} + {:ok, %{config | tests_counter: update_tests_counter(config.tests_counter, test)}} end def handle_event({:test_finished, %ExUnit.Test{state: {:skip, _}} = test}, config) do if config.trace, do: IO.puts trace_test_skip(test) - {:ok, %{config | tests_counter: config.tests_counter + 1, + {:ok, %{config | tests_counter: update_tests_counter(config.tests_counter, test), skipped_counter: config.skipped_counter + 1}} end @@ -59,7 +59,7 @@ defmodule ExUnit.CLIFormatter do IO.write invalid("?", config) end - {:ok, %{config | tests_counter: config.tests_counter + 1, + {:ok, %{config | tests_counter: update_tests_counter(config.tests_counter, test), invalids_counter: config.invalids_counter + 1}} end @@ -73,7 +73,7 @@ defmodule ExUnit.CLIFormatter do print_failure(formatted, config) print_logs(test.logs) - {:ok, %{config | tests_counter: config.tests_counter + 1, + {:ok, %{config | tests_counter: update_tests_counter(config.tests_counter, test), failures_counter: config.failures_counter + 1}} end @@ -120,6 +120,10 @@ defmodule ExUnit.CLIFormatter do end end + defp update_tests_counter(tests_counter, %{type: type} = _test) do + Map.update(tests_counter, type, 1, &(&1 + 1)) + end + ## Printing defp print_suite(config, run_us, load_us) do @@ -127,11 +131,12 @@ defmodule ExUnit.CLIFormatter do IO.puts format_time(run_us, load_us) # singular/plural - test_pl = pluralize(config.tests_counter, "test", "tests") failure_pl = pluralize(config.failures_counter, "failure", "failures") + test_type_counts = format_test_type_counts(config) + message = - "#{config.tests_counter} #{test_pl}, #{config.failures_counter} #{failure_pl}" + "#{test_type_counts} #{config.failures_counter} #{failure_pl}" |> if_true(config.skipped_counter > 0, & &1 <> ", #{config.skipped_counter} skipped") |> if_true(config.invalids_counter > 0, & &1 <> ", #{config.invalids_counter} invalid") @@ -166,6 +171,14 @@ defmodule ExUnit.CLIFormatter do IO.puts formatted end + defp format_test_type_counts(%{tests_counter: tests_counter} = _config) do + Enum.map_join tests_counter, " ", fn {type, count} -> + type_pluralized = pluralize(count, type, ExUnit.plural_rule(type |> to_string())) + + "#{count} #{type_pluralized}," + end + end + # Color styles defp colorize(escape, string, %{colors: colors}) do diff --git a/lib/ex_unit/test/ex_unit_test.exs b/lib/ex_unit/test/ex_unit_test.exs index d1b0f6b782e..e124ddb37b5 100644 --- a/lib/ex_unit/test/ex_unit_test.exs +++ b/lib/ex_unit/test/ex_unit_test.exs @@ -341,6 +341,116 @@ defmodule ExUnitTest do end) end + test "plural rules" do + on_exit fn -> + ExUnit.configure(plural_rules: %{}) + end + + word = "property" + pluralization = "properties" + + assert ExUnit.plural_rule(word) == word <> "s" + + assert ExUnit.plural_rule(word, pluralization) == :ok + + assert ExUnit.plural_rule(word) == pluralization + + invalid_word = + fn -> + ExUnit.plural_rule(:atom) + end + + assert_raise ArgumentError, "`word` must be a binary", invalid_word + + invalid_word = + fn -> + ExUnit.plural_rule(:atom, "atoms") + end + + assert_raise ArgumentError, "`word` must be a binary", invalid_word + + both_invalid = + fn -> + ExUnit.plural_rule(:atom, :atoms) + end + + assert_raise ArgumentError, "`word` must be a binary", both_invalid + + invalid_pluralization = + fn -> + ExUnit.plural_rule("atom", :atoms) + end + + assert_raise ArgumentError, "`pluralization` must be a binary", invalid_pluralization + end + + test "singular test types" do + on_exit fn -> + ExUnit.configure(plural_rules: %{}) + end + + ExUnit.plural_rule("property", "properties") + + defmodule SingularTestTypeCase do + use ExUnit.Case + + @tag type: :property + test "property is true" do + assert succeed() + end + + test "test true" do + assert succeed() + end + + defp succeed, do: true + end + + ExUnit.Server.cases_loaded() + + assert capture_io(fn -> + assert ExUnit.run == %{failures: 0, skipped: 0, total: 2} + end) =~ "1 property, 1 test, 0 failures" + end + + test "plural test types" do + on_exit fn -> + ExUnit.configure(plural_rules: %{}) + end + + ExUnit.plural_rule("property", "properties") + + defmodule PluralTestTypeCase do + use ExUnit.Case + + @tag type: :property + test "property is true" do + assert succeed() + end + + @tag type: :property + test "property is also true" do + assert succeed() + end + + test "test true" do + assert succeed() + end + + test "test true also" do + assert succeed() + end + + defp succeed, do: true + end + + ExUnit.Server.cases_loaded() + + assert capture_io(fn -> + assert ExUnit.run == %{failures: 0, skipped: 0, total: 4} + end) =~ "2 properties, 2 tests, 0 failures" + end + defp run_with_filter(filters, cases) do Enum.each(cases, &ExUnit.Server.add_sync_case/1) ExUnit.Server.cases_loaded() diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index e69417cb2d4..fcf1a999a41 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -178,7 +178,7 @@ defmodule IEx.HelpersTest do end test "s helper" do - assert capture_io(fn -> s ExUnit end) == "No specification for ExUnit was found\n" + assert capture_io(fn -> s IEx.Remsh end) == "No specification for IEx.Remsh was found\n" # Test that it shows at least two specs assert Enum.count(capture_io(fn -> s Enum end) |> String.split("\n"), fn line ->