Packages
espec
1.8.3
1.10.0
1.9.2
1.9.1
1.9.0
1.8.3
1.8.2
1.8.1
1.8.0
1.7.0
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.2
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
0.8.28
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.7
0.3.5
0.3.0
0.2.0
0.1.0
BDD testing framework for Elixir inspired by RSpec.
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/espec.ex
defmodule Mix.Tasks.Espec do
alias Mix.Utils.Stale
defmodule Cover do
@moduledoc false
def start(compile_path, opts) do
Mix.shell().info("Cover compiling modules ... ")
_ = :cover.start()
case :cover.compile_beam_directory(compile_path |> to_charlist) do
results when is_list(results) ->
:ok
{:error, _} ->
Mix.raise("Failed to cover compile directory: " <> compile_path)
end
output = opts[:output]
fn ->
Mix.shell().info("\nGenerating cover results ... ")
File.mkdir_p!(output)
Enum.each(:cover.modules(), fn mod -> cover_function(mod, output) end)
end
end
defp cover_function(mod, output) do
case :cover.analyse_to_file(mod, '#{output}/#{mod}.html', [:html]) do
{:ok, _} -> nil
{:error, error} -> Mix.shell().info("#{error} while generating cover results for #{mod}")
end
end
end
use Mix.Task
@shortdoc "Runs specs"
@preferred_cli_env :test
alias ESpec.Configuration
@moduledoc """
Runs the specs.
This task starts the current application, loads up
`spec/spec_helper.exs` and then requires all files matching the
`spec/**/_spec.exs` pattern in parallel.
A list of files can be given after the task name in order to select
the files to compile:
mix espec spec/some/particular/file_spec.exs
In case a single file is being tested, it is possible pass a specific
line number:
mix espec spec/some/particular/file_spec.exs:42
## Command line options
* `--focus` - run examples with `focus` only
* `--silent` - no output
* `--order` - run examples in the order in which they are declared
* `--sync` - run all specs synchronously ignoring 'async' tag
* `--trace` - detailed output
* `--cover` - enable code coverage
* `--only` - run only tests that match the filter `--only some:tag`
* `--exclude` - exclude tests that match the filter `--exclude some:tag`
* `--string` - run only examples whose full nested descriptions contain string `--string 'only this'`
* `--seed` - seeds the random number generator used to randomize tests order
* `--stale` - The --stale command line option attempts to run only those test files which reference modules that have changed since the last time you ran this task with --stale
## Configuration
* `:spec_paths` - list of paths containing spec files, defaults to `["spec"]`.
It is expected all spec paths to contain a `spec_helper.exs` file.
* `:spec_pattern` - a pattern to load spec files, defaults to `*_spec.exs`.
* `:test_coverage` - a set of options to be passed down to the coverage mechanism.
## Coverage
The `:test_coverage` configuration accepts the following options:
* `:output` - the output for cover results, defaults to `"cover"`
* `:tool` - the coverage tool
By default, a very simple wrapper around OTP's `cover` is used as a tool,
but it can be overridden as follows:
test_coverage: [tool: CoverModule]
`CoverModule` can be any module that exports `start/2`, receiving the
compilation path and the `test_coverage` options as arguments. It must
return an anonymous function of zero arity that will be run after the
test suite is done or `nil`.
"""
@cover [output: "cover", tool: Cover]
@recursive true
@switches [
focus: :boolean,
silent: :boolean,
order: :boolean,
sync: :boolean,
trace: :boolean,
cover: :boolean,
only: :string,
exclude: :string,
string: :string,
seed: :integer,
stale: :boolean
]
def run(args) do
{opts, files, mix_opts} = OptionParser.parse(args, strict: @switches)
check_env!()
Mix.Task.run("loadpaths", args)
if Keyword.get(mix_opts, :compile, true), do: Mix.Task.run("compile", args)
project = Mix.Project.config()
cover = Keyword.merge(@cover, project[:test_coverage] || [])
# Start cover after we load deps but before we start the app.
cover =
if opts[:cover] do
cover[:tool].start(Mix.Project.compile_path(project), cover)
end
Mix.shell().print_app
Mix.Task.run("app.start", args)
ensure_espec_loaded!()
set_configuration(opts)
success = run_espec(project, files, cover)
System.at_exit(fn _ -> unless success, do: exit({:shutdown, 1}) end)
end
def parse_files(files), do: files |> Enum.map(&parse_file(&1))
def parse_file(file) do
case Regex.run(~r/^(.+):(\d+)$/, file, capture: :all_but_first) do
[file, line_number] ->
{Path.absname(file), [line: String.to_integer(line_number)]}
nil ->
{Path.absname(file), []}
end
end
defp run_espec(project, files, cover) do
ESpec.start()
if parse_spec_files(project, files) do
success = ESpec.run()
if cover, do: cover.()
ESpec.stop()
success
else
false
end
end
defp require_spec_helper(dir) do
file = Path.join(dir, "spec_helper.exs")
if File.exists?(file) do
Code.require_file(file)
true
else
IO.puts("Cannot run tests because spec helper file `#{file}` does not exist.")
false
end
end
defp check_env! do
if elixir_version() < "1.3.0" do
unless System.get_env("MIX_ENV") || Mix.env() == :test do
Mix.raise(
"espec is running on environment #{Mix.env()}.\n" <>
"It is recommended to run espec in test environment.\n" <>
"Please add `preferred_cli_env: [espec: :test]` to project configurations in mix.exs file.\n" <>
"Or set MIX_ENV explicitly (MIX_ENV=test mix espec)"
)
end
end
end
defp elixir_version do
System.version() |> String.split("-") |> hd
end
defp ensure_espec_loaded! do
case Application.load(:espec) do
:ok -> :ok
{:error, {:already_loaded, :espec}} -> :ok
end
end
defp set_configuration(opts) do
Configuration.add(start_loading_time: :os.timestamp())
Configuration.add(opts)
end
defp parse_spec_files(project, files) do
spec_paths = get_spec_paths(project)
spec_pattern = get_spec_pattern(project)
if Enum.all?(spec_paths, &require_spec_helper(&1)) do
files_with_opts = if Enum.any?(files), do: parse_files(files), else: []
shared_spec_files = extract_shared_specs(project)
compile_result =
files_with_opts
|> Enum.map(&elem(&1, 0))
|> if_empty_use(spec_paths)
|> extract_files(spec_pattern)
|> filter_stale_files()
|> compile(include_shared: shared_spec_files)
case compile_result do
{:error, _, _} ->
false
_ ->
Configuration.add(file_opts: files_with_opts)
Configuration.add(shared_specs: shared_spec_files)
Configuration.add(finish_loading_time: :os.timestamp())
end
else
false
end
end
defp filter_stale_files(test_files) do
case Configuration.get(:stale) do
true ->
test_files
|> Stale.set_up_stale_sources()
_ ->
{test_files, []}
end
end
defp if_empty_use([], default), do: default
defp if_empty_use(value, _default), do: value
defp get_spec_paths(project) do
project[:spec_paths] || ["spec"]
end
defp get_spec_pattern(project) do
project[:spec_pattern] || "*_spec.exs"
end
defp extract_files(paths, pattern) do
already_loaded = MapSet.new(Code.required_files())
paths
|> Mix.Utils.extract_files(pattern)
|> Enum.map(&Path.absname/1)
|> Enum.reject(fn path ->
full_path = Path.expand(path)
MapSet.member?(already_loaded, full_path)
end)
end
defp extract_shared_specs(project) do
shared_spec_paths = get_shared_spec_paths(project)
shared_spec_pattern = get_shared_spec_pattern(project)
extract_files(shared_spec_paths, shared_spec_pattern)
end
defp get_shared_spec_paths(project) do
project[:shared_spec_paths] || default_shared_spec_paths(project)
end
defp default_shared_spec_paths(project) do
project
|> get_spec_paths()
|> Enum.map(&Path.join(&1, "shared"))
end
defp get_shared_spec_pattern(project) do
project[:shared_spec_pattern] || get_spec_pattern(project)
end
defp compile({spec_files, parallel_require_callbacks}, include_shared: shared_spec_files) do
shared_spec_files = shared_spec_files || []
shared_spec_files
|> Enum.each(&Code.require_file/1)
Kernel.ParallelCompiler.compile(spec_files -- shared_spec_files, parallel_require_callbacks)
end
end