Current section
Files
Jump to
Current section
Files
lib/mix/tasks/eunit.ex
defmodule Mix.Tasks.Eunit do
use Mix.Task
@recursive true
@preferred_cli_env :test
@shortdoc "Compile and run eunit tests"
@moduledoc """
Run eunit tests for a project.
This task compiles the project and its tests in the test environment,
then runs eunit tests. This task works recursively in umbrella
projects.
## Command line options
A list of patterns to match for test files can be supplied:
```
mix eunit foo* bar*
```
The runner automatically adds \".erl\" to the patterns.
The following command line switches are also available:
* `--verbose`, `-v` - run eunit with the :verbose option
* `--cover`, `-c` - create a coverage report after running the tests
* `--profile`, `-p` - show a list of the 10 slowest tests
* `--start` - start applications after compilation
* `--no-color` - disable color output
* `--force` - force compilation regardless of compilation times
* `--no-compile` - do not compile even if files require compilation
* `--no-archives-check` - do not check archives
* `--no-deps-check` - do not check dependencies
* `--no-elixir-version-check` - do not check Elixir version
The `verbose`, `cover`, `profile`, `start` and `color` switches can be set in
the `mix.exs` file and will apply to every invocation of this task. Switches
set on the command line will override any settings in the mixfile.
```
def project do
[
# ...
eunit: [
verbose: false,
cover: true,
profile: true,
start: true,
color: false
]
]
end
```
## Test search path
All \".erl\" files in the src and test directories are considered.
"""
@switches [
color: :boolean, cover: :boolean, profile: :boolean, verbose: :boolean,
start: :boolean, compile: :boolean, force: :boolean, deps_check: :boolean,
archives_check: :boolean, elixir_version_check: :boolean
]
@aliases [v: :verbose, p: :profile, c: :cover]
@default_cover_opts [output: "cover", tool: Mix.Tasks.Test.Cover]
def run(args) do
project = Mix.Project.config
options = parse_options(args, project)
# add test directory to compile paths and add
# compiler options for test
post_config = eunit_post_config(project)
modify_project_config(post_config)
if Keyword.get(options, :compile, true) do
# make sure mix will let us run compile
ensure_compile
Mix.Task.run "compile", args
end
if Keyword.get(options, :start, false) do
# start the application
Mix.shell.print_app
Mix.Task.run "app.start", args
end
# start cover
cover_state = start_cover_tool(options[:cover], project)
# run the actual tests
modules =
test_modules(post_config[:erlc_paths], options[:patterns])
|> Enum.map(&module_name_from_path/1)
|> Enum.map(fn m -> {:module, m} end)
eunit_opts = get_eunit_opts(options, post_config)
case :eunit.test(modules, eunit_opts) do
:error -> Mix.raise "mix eunit failed"
:ok -> :ok
end
analyze_coverage(cover_state)
end
defp parse_options(args, project) do
{switches, argv} =
OptionParser.parse!(args, strict: @switches, aliases: @aliases)
patterns = case argv do
[] -> ["*"]
p -> p
end
eunit_opts = case switches[:verbose] do
true -> [:verbose]
_ -> []
end
project[:eunit] || []
|> Keyword.take([:verbose, :profile, :cover, :start, :color])
|> Keyword.merge(switches)
|> Keyword.put(:eunit_opts, eunit_opts)
|> Keyword.put(:patterns, patterns)
end
defp eunit_post_config(existing_config) do
[erlc_paths: existing_config[:erlc_paths] ++ ["test"],
erlc_options: existing_config[:erlc_options] ++ [{:d, :TEST}],
eunit_opts: existing_config[:eunit_opts] || []]
end
defp get_eunit_opts(options, post_config) do
eunit_opts = options[:eunit_opts] ++ post_config[:eunit_opts]
maybe_add_formatter(eunit_opts, options[:profile], options[:color] || true)
end
defp maybe_add_formatter(opts, profile, color) do
if Keyword.has_key?(opts, :report) do
opts
else
format_opts = color_opt(color) ++ profile_opt(profile)
[:no_tty, {:report, {:eunit_progress, format_opts}} | opts]
end
end
defp color_opt(true), do: [:colored]
defp color_opt(_), do: []
defp profile_opt(true), do: [:profile]
defp profile_opt(_), do: []
defp modify_project_config(post_config) do
# note - we have to grab build_path because
# Mix.Project.push resets the build path
build_path = Mix.Project.build_path
|> Path.split
|> Enum.map(fn(p) -> filter_replace(p, "dev", "eunit") end)
|> Path.join
%{name: name, file: file} = Mix.Project.pop
Mix.ProjectStack.post_config(Keyword.merge(post_config,
[build_path: build_path]))
Mix.Project.push name, file
end
defp filter_replace(x, x, r) do
r
end
defp filter_replace(x, _y, _r) do
x
end
defp ensure_compile do
# we have to reenable compile and all of its
# child tasks (compile.erlang, compile.elixir, etc)
Mix.Task.reenable("compile")
Enum.each(compilers, &Mix.Task.reenable/1)
end
defp compilers do
Mix.Task.all_modules
|> Enum.map(&Mix.Task.task_name/1)
|> Enum.filter(fn(t) -> match?("compile." <> _, t) end)
end
defp test_modules(directories, patterns) do
all_modules = erlang_source_files(directories, patterns)
|> Enum.map(&module_name_from_path/1)
|> Enum.uniq
remove_test_duplicates(all_modules, all_modules, [])
end
defp erlang_source_files(directories, patterns) do
Enum.map(patterns, fn(p) ->
Mix.Utils.extract_files(directories, p <> ".erl")
end)
|> Enum.concat
|> Enum.uniq
end
defp module_name_from_path(p) do
Path.basename(p, ".erl") |> String.to_atom
end
defp remove_test_duplicates([], _all_module_names, accum) do
accum
end
defp remove_test_duplicates([module | rest], all_module_names, accum) do
module = Atom.to_string(module)
if tests_module?(module) &&
Enum.member?(all_module_names, without_test_suffix(module)) do
remove_test_duplicates(rest, all_module_names, accum)
else
remove_test_duplicates(rest, all_module_names, [module | accum])
end
end
defp tests_module?(module_name) do
String.match?(module_name, ~r/_tests$/)
end
defp without_test_suffix(module_name) do
module_name
|> String.replace(~r/_tests$/, "")
|> String.to_atom
end
# coverage was disabled
defp start_cover_tool(nil, _project), do: nil
defp start_cover_tool(false, _project), do: nil
# set up the cover tool
defp start_cover_tool(_cover_switch, project) do
compile_path = Mix.Project.compile_path(project)
cover = Keyword.merge(@default_cover_opts, project[:test_coverage] || [])
# returns a callback
cover[:tool].start(compile_path, cover)
end
# no cover tool was specified
defp analyze_coverage(nil), do: :ok
# run the cover callback
defp analyze_coverage(cb), do: cb.()
end