Packages
espec
1.8.0
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/utils/stale.ex
defmodule Mix.Utils.Stale do
import Record
require Mix.Compilers.Elixir, as: CE
defrecordp :source,
source: nil,
compile_references: [],
runtime_references: [],
external: []
@stale_manifest "compile.espec_stale"
@manifest_vsn 1
@agent_manifest_name :espec_stale_manifest_agent
# set up stale manifest
def set_up_stale_sources(matched_test_files) do
manifest = manifest()
modified = Mix.Utils.last_modified(manifest)
all_sources = read_manifest()
removed =
for source(source: source) <- all_sources,
source not in matched_test_files,
do: Path.expand(source)
sources_mtimes = mtimes(all_sources)
# Let's start with the new sources
# Plus the sources that have changed in disk
changed =
for(
source <- matched_test_files,
not List.keymember?(all_sources, source, source(:source)),
do: source
) ++
for(
source(source: source, external: external) <- all_sources,
times = Enum.map([source | external], &Map.fetch!(sources_mtimes, &1)),
Mix.Utils.stale?(times, [modified]),
do: source
)
stale = MapSet.new(changed -- removed)
sources = update_stale_sources(all_sources, removed, changed)
test_files_to_run =
sources
|> tests_with_changed_references()
|> MapSet.union(stale)
|> MapSet.to_list()
if test_files_to_run == [] do
{[], []}
else
{:ok, pid} = Agent.start_link(fn -> sources end, name: @agent_manifest_name)
cwd = File.cwd!()
parallel_require_callbacks = parse_version() |> parallel_require_callbacks(pid, cwd)
{test_files_to_run, parallel_require_callbacks}
end
end
## Parallel Require Callback dependent on Elixir version
defp parallel_require_callbacks(%Version{major: 1, minor: minor} = version, pid, cwd)
when minor >= 9,
do: [
each_module: &each_module(version, pid, cwd, &1, &2, &3),
each_file: &each_file(pid, &1, &2)
]
defp parallel_require_callbacks(%Version{major: 1, minor: minor} = version, pid, cwd)
when minor >= 6 and minor < 9 do
[each_module: &each_module(version, pid, cwd, &1, &2, &3)]
end
defp parallel_require_callbacks(_, _, _),
do: {:error, "Your version of Elixir #{System.version()} cannot support the stale feature"}
defp parse_version do
System.version()
|> Version.parse()
|> case do
{:ok, version} -> version
:error -> :error
end
end
## Manifest
def write_manifest([]) do
File.rm(manifest())
:ok
end
def write_manifest(sources) do
manifest = manifest()
File.mkdir_p!(Path.dirname(manifest))
manifest_data = :erlang.term_to_binary([@manifest_vsn | sources], [:compressed])
File.write!(manifest, manifest_data)
end
defp manifest, do: Path.join(Mix.Project.manifest_path(), @stale_manifest)
defp read_manifest() do
try do
[@manifest_vsn | sources] = manifest() |> File.read!() |> :erlang.binary_to_term()
sources
rescue
_ -> []
end
end
def agent_manifest_name(), do: @agent_manifest_name
def agent_write_manifest() do
Agent.cast(@agent_manifest_name, fn sources ->
write_manifest(sources)
sources
end)
end
## Setup helpers
defp mtimes(sources) do
Enum.reduce(sources, %{}, fn source(source: source, external: external), map ->
Enum.reduce([source | external], map, fn file, map ->
Map.put_new_lazy(map, file, fn -> Mix.Utils.last_modified(file) end)
end)
end)
end
defp update_stale_sources(sources, removed, changed) do
sources = Enum.reject(sources, fn source(source: source) -> source in removed end)
sources =
Enum.reduce(changed, sources, &List.keystore(&2, &1, source(:source), source(source: &1)))
sources
end
## Test changed dependency resolution
defp tests_with_changed_references(test_sources) do
test_manifest = manifest()
[elixir_manifest] = Mix.Tasks.Compile.Elixir.manifests()
if Mix.Utils.stale?([elixir_manifest], [test_manifest]) do
elixir_manifest_entries =
CE.read_manifest(elixir_manifest, Mix.Project.compile_path())
|> Enum.group_by(&elem(&1, 0))
stale_modules =
for CE.module(module: module, beam: beam) <- elixir_manifest_entries.module,
Mix.Utils.stale?([beam], [test_manifest]),
do: module,
into: MapSet.new()
stale_modules =
find_all_dependent_on(
stale_modules,
elixir_manifest_entries.source,
elixir_manifest_entries.module
)
for module <- stale_modules,
source(source: source, runtime_references: r, compile_references: c) <- test_sources,
module in r or module in c,
do: source,
into: MapSet.new()
else
MapSet.new()
end
end
defp find_all_dependent_on(modules, sources, all_modules, resolved \\ MapSet.new()) do
new_modules =
for module <- modules,
module not in resolved,
dependent_module <- dependent_modules(module, all_modules, sources),
do: dependent_module,
into: modules
if MapSet.size(new_modules) == MapSet.size(modules) do
new_modules
else
find_all_dependent_on(new_modules, sources, all_modules, modules)
end
end
defp dependent_modules(module, modules, sources) do
for CE.source(source: source, runtime_references: r, compile_references: c) <- sources,
module in r or module in c,
CE.module(sources: sources, module: dependent_module) <- modules,
source in sources,
do: dependent_module
end
## ParallelRequire callback: Handled differently depending on Elixir version
defp each_module(%Version{major: 1, minor: minor}, pid, cwd, file, module, _binary)
when minor >= 9 do
external = get_external_resources(module, cwd)
if external != [] do
Agent.update(pid, fn sources ->
file = Path.relative_to(file, cwd)
{source, sources} = List.keytake(sources, file, source(:source))
[source(source, external: external ++ source(source, :external)) | sources]
end)
end
:ok
end
defp each_module(%Version{major: 1, minor: minor}, pid, cwd, source, module, _binary)
when minor >= 6 and minor < 9 do
{compile_references, struct_references, runtime_references} =
Kernel.LexicalTracker.remote_references(module)
external = get_external_resources(module, cwd)
source = Path.relative_to(source, cwd)
Agent.cast(pid, fn sources ->
external =
case List.keyfind(sources, source, source(:source)) do
source(external: old_external) -> external ++ old_external
nil -> external
end
new_source =
source(
source: source,
compile_references: compile_references ++ struct_references,
runtime_references: runtime_references,
external: external
)
List.keystore(sources, source, source(:source), new_source)
end)
end
defp each_file(pid, file, lexical) do
Agent.update(pid, fn sources ->
case List.keytake(sources, file, source(:source)) do
{source, sources} ->
{compile_references, struct_references, runtime_references} =
Kernel.LexicalTracker.remote_references(lexical)
source =
source(
source,
compile_references: compile_references ++ struct_references,
runtime_references: runtime_references
)
[source | sources]
nil ->
sources
end
end)
end
defp get_external_resources(module, cwd) do
for file <- Module.get_attribute(module, :external_resource), do: Path.relative_to(file, cwd)
end
end