Current section

Files

Jump to
espec lib mix utils stale.ex
Raw

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