Current section

Files

Jump to
al_check lib check runner.ex
Raw

lib/check/runner.ex

defmodule CheckEscript.Runner do
@moduledoc "Parallel check execution, streaming, and port management."
alias CheckEscript.{Failed, Tasks, UI}
def run_checks(tasks, test_opts, test_cmd, max_concurrency, verbose) do
print_header(tasks, test_cmd)
start_time = System.monotonic_time(:millisecond)
{:ok, dot_counter_pid} = Agent.start_link(fn -> %{} end)
results = execute_tasks(tasks, dot_counter_pid, max_concurrency, verbose, test_opts)
Agent.stop(dot_counter_pid)
total_seconds = elapsed_seconds(start_time)
{results, total_seconds}
end
def stream_port_output(port) do
spinner = CheckEscript.Spinner.start()
status = do_stream_port_output(port, spinner)
CheckEscript.Spinner.stop(spinner)
status
end
defp do_stream_port_output(port, spinner) do
receive do
{^port, {:data, data}} ->
CheckEscript.Spinner.stop(spinner)
IO.binwrite(:stdio, data)
new_spinner = CheckEscript.Spinner.start()
do_stream_port_output(port, new_spinner)
{^port, {:exit_status, status}} ->
status
end
end
def run_check(cmd, args, false) do
{output, status} = System.cmd(cmd, args, stderr_to_stdout: true)
{status, output}
end
def run_check(cmd, args, true) do
port = CheckEscript.Port.open(cmd, args)
collect_and_stream_output(port, "")
end
defp run_builtin("modified_tests", opts), do: CheckEscript.ModifiedTests.run(opts)
defp run_builtin("modified_test_modules", opts), do: CheckEscript.ModifiedTestModules.run(opts)
defp run_builtin(name, _opts) do
IO.puts(:stderr, "Unknown builtin: #{name}")
{1, ""}
end
# -- Private --
defp print_header(tasks, test_cmd) do
IO.puts("Running code quality checks in parallel...\n")
if Tasks.has_test_tasks?(tasks) do
IO.puts([IO.ANSI.format([:cyan, "Test command: #{test_cmd}\n"])])
end
schedulers = :erlang.system_info(:schedulers_online)
IO.puts("Available schedulers: #{schedulers}\n")
end
defp execute_tasks(tasks, dot_counter_pid, max_concurrency, verbose, test_opts) do
task_count = length(tasks)
if not verbose do
Enum.each(tasks, fn task ->
IO.puts(" • #{String.pad_trailing(elem(task, 0), 25)} [RUNNING]")
end)
end
tasks
|> Enum.with_index()
|> Task.async_stream(
fn {task, index} ->
execute_task(task, index, task_count, dot_counter_pid, verbose, test_opts)
end,
timeout: :infinity,
ordered: false,
max_concurrency: max_concurrency
)
|> Enum.map(fn {:ok, {name, index, status, output, test_count}} ->
if not verbose, do: UI.update_task_line(index, name, status, task_count, test_count)
{name, status, output}
end)
end
defp execute_task(
{name, cmd, args, partition, total_partitions},
index,
task_count,
dot_counter_pid,
verbose,
_test_opts
) do
Process.sleep((rem(index, total_partitions) + 1) * 200)
{status, output} =
run_check_with_streaming(
cmd,
args,
index,
name,
task_count,
dot_counter_pid,
partition,
verbose
)
{total, failures} =
Agent.get(dot_counter_pid, fn state ->
{Map.get(state, "partition_#{partition}_total", 0),
Map.get(state, "partition_#{partition}_failures", 0)}
end)
{name, index, status, output, {total, failures}}
end
defp execute_task(
{name, :builtin, args},
index,
_task_count,
_dot_counter_pid,
_verbose,
test_opts
) do
{status, output} = run_builtin(hd(args), test_opts)
{name, index, status, output, nil}
end
defp execute_task({name, cmd, args}, index, _task_count, _dot_counter_pid, verbose, _test_opts) do
{status, output} = run_check(cmd, args, verbose)
{name, index, status, output, nil}
end
defp run_check_with_streaming(
cmd,
args,
index,
name,
total_tasks,
dot_counter_pid,
partition,
verbose
) do
updater_pid =
if not verbose do
spawn_link(fn -> UI.update_loop(index, name, total_tasks, dot_counter_pid) end)
end
File.mkdir_p!(".check")
file_handle = File.open!(".check/test_partition_#{partition}.txt", [:write, :utf8])
port = CheckEscript.Port.open(cmd, args)
{output, status} =
collect_port_output(port, "", dot_counter_pid, partition, file_handle, verbose)
File.close(file_handle)
if updater_pid, do: send(updater_pid, :stop)
{determine_final_status(status, output), output}
end
def determine_final_status(status, output) do
tests_passed? = Regex.match?(~r/\d+ tests?, 0 failures/, output)
cond do
status == 0 and Failed.detect_warnings_in_output(output) -> :warnings
status != 0 and tests_passed? and Failed.detect_warnings_in_output(output) -> :warnings
status != 0 and Failed.coverage_threshold_failure?(output) -> 0
true -> status
end
end
defp collect_and_stream_output(port, acc) do
receive do
{^port, {:data, data}} ->
IO.binwrite(:stdio, data)
collect_and_stream_output(port, acc <> data)
{^port, {:exit_status, status}} ->
{status, acc}
end
end
defp collect_port_output(port, acc, dot_counter_pid, partition, file_handle, verbose) do
receive do
{^port, {:data, data}} ->
IO.write(file_handle, data)
if verbose, do: IO.binwrite(:stdio, data)
update_test_progress(data, dot_counter_pid, partition)
collect_port_output(port, acc <> data, dot_counter_pid, partition, file_handle, verbose)
{^port, {:exit_status, status}} ->
{acc, status}
end
end
defp update_test_progress(data, dot_counter_pid, partition) do
total_count = count_test_dots(data) + count_failure_markers(data)
if total_count > 0 do
Agent.update(dot_counter_pid, fn state ->
Map.update(state, "partition_#{partition}_total", total_count, &(&1 + total_count))
end)
end
parse_test_summary(data, dot_counter_pid, partition)
end
defp count_test_dots(data) do
data
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.filter(&String.match?(&1, ~r/^[\.\*F]+$/))
|> Enum.map(fn line -> line |> String.graphemes() |> Enum.count(&(&1 == ".")) end)
|> Enum.sum()
end
defp count_failure_markers(data) do
data
|> String.split("\n")
|> Enum.count(&Regex.match?(~r/^\s+\d+\)\s+test\s+/, &1))
end
defp parse_test_summary(data, dot_counter_pid, partition) do
case Regex.run(~r/(\d+) tests?, (\d+) failures?/, data) do
[_, total_str, failures_str] ->
Agent.update(dot_counter_pid, fn state ->
state
|> Map.put("partition_#{partition}_total", String.to_integer(total_str))
|> Map.put("partition_#{partition}_failures", String.to_integer(failures_str))
end)
_ ->
:ok
end
end
defp elapsed_seconds(start_time) do
(System.monotonic_time(:millisecond) - start_time)
|> Kernel./(1000)
|> Float.round(1)
end
end