defmodule Mix.Tasks.Test.Seq do @shortdoc "Run tests sequentially, one file at a time" @moduledoc """ Runs all test files sequentially, each in its own OS process. This avoids SQLite's global VFS contention that causes spurious "out of memory" errors when test files run in parallel. ## Usage mix test.seq mix test.seq --trace mix test.seq --cover # per-file .coverdata exports under cover/ With `--cover`, each file's OS process exports a distinctly named `.coverdata` (derived from the file path), so runs don't overwrite each other. Merge and publish afterwards, e.g. `mix coveralls.github --import-cover cover test/xqlite_test.exs`. """ use Mix.Task def run(args) do test_files = find_test_files() IO.puts("Found #{length(test_files)} test files") IO.puts("Running tests sequentially...") failed_files = run_test_files(test_files, args, []) if failed_files == [] do IO.puts("\nāœ“ All tests passed!") else IO.puts("\nFailed files: #{Enum.join(failed_files, ", ")}") Mix.raise("#{length(failed_files)} test file(s) failed") end end defp run_test_files([], _args, failed_files), do: failed_files defp run_test_files([file | rest], args, failed_files) do IO.puts("\n=== Running #{file} ===") case System.cmd("mix", ["test", file] ++ args ++ coverage_args(args, file), into: IO.stream() ) do {_, 0} -> IO.puts("āœ“ #{file} passed") run_test_files(rest, args, failed_files) {_, exit_code} -> IO.puts("āœ— #{file} failed (exit code: #{exit_code})") run_test_files(rest, args, [file | failed_files]) end end # Distinct export names per file (full-path-derived — bare basenames # collide, e.g. test/pragma_test.exs vs test/nif/pragma_test.exs). defp coverage_args(args, file) do if "--cover" in args do ["--export-coverage", String.replace(Path.rootname(file), ["/", "\\"], "_")] else [] end end defp find_test_files do Path.wildcard("test/**/*_test.exs") |> Enum.sort() end end