├── .gitignore ├── LICENSE ├── README.md ├── lib └── mix │ └── tasks │ └── eunit.ex ├── mix.exs └── mix.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | doc 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Dan Swain 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MixEunit 2 | ======== 3 | 4 | A mix task to execute eunit tests. 5 | 6 | * Works in umbrella projects. 7 | * Tests can be in the module or in the test directory. 8 | * Allows the user to provide a list of patterns for tests to run. 9 | 10 | Example 11 | ``` 12 | mix eunit # run all the tests 13 | mix eunit --verbose "foo*" "*_test" # verbose run foo*.erl and *_test.erl 14 | ``` 15 | 16 | Installation 17 | ------------ 18 | 19 | Add to your `mix.exs` deps: 20 | 21 | ```elixir 22 | def deps 23 | [#... existing deps, 24 | {:mix_eunit, "~> 0.2"}] 25 | end 26 | ``` 27 | 28 | Then 29 | 30 | ``` 31 | mix deps.get 32 | mix deps.compile 33 | mix eunit 34 | ``` 35 | 36 | To make the `eunit` task run in the `:test` environment, add the following 37 | to the `project` section of you mix file: 38 | 39 | ```elixir 40 | def project 41 | [#... existing project settings, 42 | preferred_cli_env: [eunit: :test] 43 | ] 44 | end 45 | ``` 46 | 47 | Command line options: 48 | --------------------- 49 | 50 | A list of patterns to match for test files can be supplied: 51 | 52 | ``` 53 | mix eunit foo* bar* 54 | ``` 55 | 56 | The runner automatically adds ".erl" to the patterns. 57 | 58 | The following command line switch is also available: 59 | 60 | * `--verbose`, `-v` - run eunit with the :verbose option 61 | * `--cover`, `-c` - create a coverage report after running the tests 62 | * `--profile`, `-p` - show a list of the 10 slowest tests 63 | * `--start` - start applications after compilation 64 | * `--no-color` - disable color output 65 | * `--force` - force compilation regardless of compilation times 66 | * `--no-compile` - do not compile even if files require compilation 67 | * `--no-archives-check` - do not check archives 68 | * `--no-deps-check` - do not check dependencies 69 | * `--no-elixir-version-check` - do not check Elixir version 70 | 71 | Project Settings: 72 | ----------------- 73 | 74 | The following `mix.exs` project settings affect the behavior of `mix eunit`. 75 | 76 | ```elixir 77 | def project 78 | [ 79 | # existing project settings 80 | 81 | # run the `eunit` task in the `:test` environment 82 | preferred_cli_env: [eunit: :test], 83 | 84 | # set the output directory for `mix eunit --cover` reports 85 | # the default is `./cover` (same as `mix test --cover`) 86 | test_coverage: [output: "_build/#{Mix.env}/cover"] 87 | 88 | # set switches that affect every invocation of the eunit task 89 | eunit: [ 90 | verbose: false, 91 | cover: true, 92 | profile: true, 93 | start: true, 94 | color: false 95 | ] 96 | ] 97 | end 98 | ``` 99 | 100 | Cover: 101 | ------ 102 | 103 | Note that `mix eunit --cover` and `mix test --cover` will not in 104 | general cover the same source code with tests and therefore will 105 | typically generate two independent coverage reports. There may be 106 | some overlap if your eunit and ExUnit tests cover some of the same 107 | code. 108 | 109 | By default, `mix eunit --cover` produces coverage reports in HTML 110 | format in the same `cover` directory that `mix test --cover` does. 111 | You can override the `mix eunit --cover` output directory in your 112 | `mix.exs` file as described above. 113 | 114 | Test search path: 115 | ----------------- 116 | 117 | All ".erl" files in the src and test directories are considered. 118 | 119 | -------------------------------------------------------------------------------- /lib/mix/tasks/eunit.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Eunit do 2 | use Mix.Task 3 | @recursive true 4 | 5 | @preferred_cli_env :test 6 | 7 | @shortdoc "Compile and run eunit tests" 8 | 9 | @moduledoc """ 10 | Run eunit tests for a project. 11 | 12 | This task compiles the project and its tests in the test environment, 13 | then runs eunit tests. This task works recursively in umbrella 14 | projects. 15 | 16 | 17 | ## Command line options 18 | 19 | A list of patterns to match for test files can be supplied: 20 | 21 | ``` 22 | mix eunit foo* bar* 23 | ``` 24 | 25 | The runner automatically adds \".erl\" to the patterns. 26 | 27 | The following command line switches are also available: 28 | 29 | * `--verbose`, `-v` - run eunit with the :verbose option 30 | * `--cover`, `-c` - create a coverage report after running the tests 31 | * `--profile`, `-p` - show a list of the 10 slowest tests 32 | * `--start` - start applications after compilation 33 | * `--no-color` - disable color output 34 | * `--force` - force compilation regardless of compilation times 35 | * `--no-compile` - do not compile even if files require compilation 36 | * `--no-archives-check` - do not check archives 37 | * `--no-deps-check` - do not check dependencies 38 | * `--no-elixir-version-check` - do not check Elixir version 39 | 40 | The `verbose`, `cover`, `profile`, `start` and `color` switches can be set in 41 | the `mix.exs` file and will apply to every invocation of this task. Switches 42 | set on the command line will override any settings in the mixfile. 43 | 44 | ``` 45 | def project do 46 | [ 47 | # ... 48 | eunit: [ 49 | verbose: false, 50 | cover: true, 51 | profile: true, 52 | start: true, 53 | color: false 54 | ] 55 | ] 56 | end 57 | ``` 58 | 59 | ## Test search path 60 | 61 | All \".erl\" files in the src and test directories are considered. 62 | 63 | """ 64 | 65 | @switches [ 66 | color: :boolean, cover: :boolean, profile: :boolean, verbose: :boolean, 67 | start: :boolean, compile: :boolean, force: :boolean, deps_check: :boolean, 68 | archives_check: :boolean, elixir_version_check: :boolean 69 | ] 70 | 71 | @aliases [v: :verbose, p: :profile, c: :cover] 72 | 73 | @default_cover_opts [output: "cover", tool: Mix.Tasks.Test.Cover] 74 | 75 | def run(args) do 76 | project = Mix.Project.config 77 | options = parse_options(args, project) 78 | 79 | # add test directory to compile paths and add 80 | # compiler options for test 81 | post_config = eunit_post_config(project) 82 | modify_project_config(post_config) 83 | 84 | if Keyword.get(options, :compile, true) do 85 | # make sure mix will let us run compile 86 | ensure_compile() 87 | Mix.Task.run "compile", args 88 | end 89 | 90 | if Keyword.get(options, :start, false) do 91 | # start the application 92 | Mix.shell.print_app 93 | Mix.Task.run "app.start", args 94 | end 95 | 96 | # start cover 97 | cover_state = start_cover_tool(options[:cover], project) 98 | 99 | # run the actual tests 100 | modules = 101 | test_modules(post_config[:erlc_paths], options[:patterns]) 102 | |> Enum.map(&module_name_from_path/1) 103 | |> Enum.map(fn m -> {:module, m} end) 104 | 105 | eunit_opts = get_eunit_opts(options, post_config) 106 | case :eunit.test(modules, eunit_opts) do 107 | :error -> Mix.raise "mix eunit failed" 108 | :ok -> :ok 109 | end 110 | 111 | analyze_coverage(cover_state) 112 | end 113 | 114 | defp parse_options(args, project) do 115 | {switches, argv, errors} = 116 | OptionParser.parse(args, strict: @switches, aliases: @aliases) 117 | if errors != [], do: raise OptionParser.ParseError, "#{inspect errors}" 118 | 119 | patterns = case argv do 120 | [] -> ["*"] 121 | p -> p 122 | end 123 | 124 | eunit_opts = case switches[:verbose] do 125 | true -> [:verbose] 126 | _ -> [] 127 | end 128 | 129 | project[:eunit] || [] 130 | |> Keyword.take([:verbose, :profile, :cover, :start, :color]) 131 | |> Keyword.merge(switches) 132 | |> Keyword.put(:eunit_opts, eunit_opts) 133 | |> Keyword.put(:patterns, patterns) 134 | end 135 | 136 | defp eunit_post_config(existing_config) do 137 | [erlc_paths: existing_config[:erlc_paths] ++ ["test"], 138 | erlc_options: existing_config[:erlc_options] ++ [{:d, :TEST}], 139 | eunit_opts: existing_config[:eunit_opts] || []] 140 | end 141 | 142 | defp get_eunit_opts(options, post_config) do 143 | eunit_opts = options[:eunit_opts] ++ post_config[:eunit_opts] 144 | maybe_add_formatter(eunit_opts, options[:profile], options[:color] || true) 145 | end 146 | 147 | defp maybe_add_formatter(opts, profile, color) do 148 | if Keyword.has_key?(opts, :report) do 149 | opts 150 | else 151 | format_opts = color_opt(color) ++ profile_opt(profile) 152 | [:no_tty, {:report, {:eunit_progress, format_opts}} | opts] 153 | end 154 | end 155 | 156 | defp color_opt(true), do: [:colored] 157 | defp color_opt(_), do: [] 158 | 159 | defp profile_opt(true), do: [:profile] 160 | defp profile_opt(_), do: [] 161 | 162 | defp modify_project_config(post_config) do 163 | # note - we have to grab build_path because 164 | # Mix.Project.push resets the build path 165 | build_path = Mix.Project.build_path 166 | |> Path.split 167 | |> Enum.map(fn(p) -> filter_replace(p, "dev", "eunit") end) 168 | |> Path.join 169 | 170 | %{name: name, file: file} = Mix.Project.pop 171 | Mix.ProjectStack.post_config(Keyword.merge(post_config, 172 | [build_path: build_path])) 173 | Mix.Project.push name, file 174 | end 175 | 176 | defp filter_replace(x, x, r) do 177 | r 178 | end 179 | defp filter_replace(x, _y, _r) do 180 | x 181 | end 182 | 183 | defp ensure_compile do 184 | # we have to reenable compile and all of its 185 | # child tasks (compile.erlang, compile.elixir, etc) 186 | Mix.Task.reenable("compile") 187 | Enum.each(compilers(), &Mix.Task.reenable/1) 188 | end 189 | 190 | defp compilers do 191 | Mix.Task.all_modules 192 | |> Enum.map(&Mix.Task.task_name/1) 193 | |> Enum.filter(fn(t) -> match?("compile." <> _, t) end) 194 | end 195 | 196 | defp test_modules(directories, patterns) do 197 | all_modules = erlang_source_files(directories, patterns) 198 | |> Enum.map(&module_name_from_path/1) 199 | |> Enum.uniq 200 | 201 | remove_test_duplicates(all_modules, all_modules, []) 202 | end 203 | 204 | defp erlang_source_files(directories, patterns) do 205 | Enum.map(patterns, fn(p) -> 206 | Mix.Utils.extract_files(directories, p <> ".erl") 207 | end) 208 | |> Enum.concat 209 | |> Enum.uniq 210 | end 211 | 212 | defp module_name_from_path(p) do 213 | Path.basename(p, ".erl") |> String.to_atom 214 | end 215 | 216 | defp remove_test_duplicates([], _all_module_names, accum) do 217 | accum 218 | end 219 | defp remove_test_duplicates([module | rest], all_module_names, accum) do 220 | module = Atom.to_string(module) 221 | if tests_module?(module) && 222 | Enum.member?(all_module_names, without_test_suffix(module)) do 223 | remove_test_duplicates(rest, all_module_names, accum) 224 | else 225 | remove_test_duplicates(rest, all_module_names, [module | accum]) 226 | end 227 | end 228 | 229 | defp tests_module?(module_name) do 230 | String.match?(module_name, ~r/_tests$/) 231 | end 232 | 233 | defp without_test_suffix(module_name) do 234 | module_name 235 | |> String.replace(~r/_tests$/, "") 236 | |> String.to_atom 237 | end 238 | 239 | # coverage was disabled 240 | defp start_cover_tool(nil, _project), do: nil 241 | defp start_cover_tool(false, _project), do: nil 242 | # set up the cover tool 243 | defp start_cover_tool(_cover_switch, project) do 244 | compile_path = Mix.Project.compile_path(project) 245 | cover = Keyword.merge(@default_cover_opts, project[:test_coverage] || []) 246 | # returns a callback 247 | cover[:tool].start(compile_path, cover) 248 | end 249 | 250 | # no cover tool was specified 251 | defp analyze_coverage(nil), do: :ok 252 | # run the cover callback 253 | defp analyze_coverage(cb), do: cb.() 254 | end 255 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MixEunit.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :mix_eunit, 6 | version: "0.3.0", 7 | elixir: "~> 1.0", 8 | description: "A mix task to run eunit tests, works for umbrella projects", 9 | package: package(), 10 | deps: deps()] 11 | end 12 | 13 | defp package do 14 | [ 15 | files: [ 16 | "LICENSE", 17 | "mix.exs", 18 | "README.md", 19 | "lib" 20 | ], 21 | maintainers: ["Dan Swain"], 22 | links: %{"github" => "https://github.com/dantswain/mix_eunit"}, 23 | licenses: ["MIT"] 24 | ] 25 | end 26 | 27 | defp deps do 28 | [ 29 | {:eunit_formatters, "~> 0.3.1"}, 30 | {:ex_doc, ">= 0.0.0", only: :dev} 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], []}, 2 | "eunit_formatters": {:hex, :eunit_formatters, "0.3.1", "7a6fc351eb5b873e2356b8852eb751e20c13a72fbca03393cf682b8483509573", [:rebar3], []}, 3 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}} 4 | --------------------------------------------------------------------------------