Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/raxol.test_parallel.ex.disabled
defmodule Mix.Tasks.Raxol.TestParallel do
use Mix.Task
@shortdoc "Run tests with optimized parallel execution"
@moduledoc """
Optimized parallel test execution for Raxol.
This task runs tests with intelligent partitioning to maximize
parallel execution while avoiding resource contention.
## Usage
mix raxol.test_parallel [options]
## Options
* `--max-cases` - Maximum number of parallel test cases (default: auto-detect)
* `--partition-by` - Partition strategy: module, file, or test (default: module)
* `--timeout` - Test timeout in milliseconds (default: 60000)
* `--include` - Include tests matching pattern
* `--exclude` - Exclude tests matching pattern
* `--only` - Run only tests matching pattern
* `--seed` - Random seed for test order
* `--trace` - Enable detailed test tracing
* `--profile` - Profile test execution performance
## Examples
# Run with auto-detected parallel workers
mix raxol.test_parallel
# Run with specific number of workers
mix raxol.test_parallel --max-cases 8
# Run with file-level partitioning
mix raxol.test_parallel --partition-by file
# Run excluding slow tests with profiling
mix raxol.test_parallel --exclude slow --profile
## Performance Optimizations
1. **Intelligent Partitioning**: Groups tests by resource usage patterns
2. **Load Balancing**: Distributes test load across available cores
3. **Resource Isolation**: Prevents conflicts between parallel tests
4. **Memory Management**: Monitors memory usage during execution
5. **Adaptive Scheduling**: Adjusts parallelism based on system load
"""
@switches [
max_cases: :integer,
partition_by: :string,
timeout: :integer,
include: :keep,
exclude: :keep,
only: :keep,
seed: :integer,
trace: :boolean,
profile: :boolean,
help: :boolean
]
@aliases [
h: :help
]
def run(args) do
{options, remaining_args, _} = OptionParser.parse(args, switches: @switches, aliases: @aliases)
if options[:help] do
print_help()
return
end
# Ensure test environment
Mix.env(:test)
# Start the application for testing
Mix.Task.run("loadpaths")
Mix.Task.run("compile", remaining_args)
# Configure parallel execution
config = configure_parallel_execution(options)
# Profile if requested
profiler_pid = if options[:profile] do
start_profiler()
end
try do
# Run optimized parallel tests
run_parallel_tests(config, remaining_args)
after
if profiler_pid do
stop_profiler(profiler_pid)
end
end
end
defp print_help do
IO.puts(@moduledoc)
end
defp configure_parallel_execution(options) do
max_cases = options[:max_cases] || detect_optimal_parallelism()
partition_strategy = String.to_atom(options[:partition_by] || "module")
timeout = options[:timeout] || 60_000
%{
max_cases: max_cases,
partition_strategy: partition_strategy,
timeout: timeout,
include: options[:include] || [],
exclude: options[:exclude] || [],
only: options[:only] || [],
seed: options[:seed],
trace: options[:trace] || false,
profile: options[:profile] || false
}
end
defp detect_optimal_parallelism do
# Get available CPU cores
cpu_cores = System.schedulers_online()
# Get available memory (in GB)
memory_gb = get_available_memory_gb()
# Calculate optimal parallel workers
# Rule: 1 worker per core, but limit based on memory
# Each test process may use ~50MB peak
memory_limit = trunc(memory_gb * 1024 / 50)
optimal_workers = min(cpu_cores, memory_limit)
# Ensure at least 2 workers, max 16 for reasonable limits
max(2, min(16, optimal_workers))
end
defp get_available_memory_gb do
case :os.type() do
{:unix, :darwin} ->
# macOS
case System.cmd("sysctl", ["-n", "hw.memsize"]) do
{output, 0} ->
output
|> String.trim()
|> String.to_integer()
|> div(1024 * 1024 * 1024)
_ -> 8 # Default to 8GB
end
{:unix, :linux} ->
# Linux
case File.read("/proc/meminfo") do
{:ok, content} ->
content
|> String.split("\n")
|> Enum.find(&String.starts_with?(&1, "MemAvailable:"))
|> case do
nil -> 8
line ->
line
|> String.split()
|> Enum.at(1)
|> String.to_integer()
|> div(1024 * 1024) # Convert KB to GB
end
_ -> 8
end
_ -> 8 # Default for other systems
end
end
defp run_parallel_tests(config, args) do
IO.puts("š Starting optimized parallel test execution...")
IO.puts(" Max workers: #{config.max_cases}")
IO.puts(" Partition strategy: #{config.partition_strategy}")
IO.puts(" Timeout: #{config.timeout}ms")
# Discover all test files
test_files = discover_test_files()
# Partition tests based on strategy
partitions = partition_tests(test_files, config.partition_strategy)
IO.puts(" Test partitions: #{length(partitions)}")
# Set ExUnit configuration
ExUnit.configure([
max_cases: config.max_cases,
timeout: config.timeout,
trace: config.trace,
seed: config.seed,
include: config.include,
exclude: config.exclude,
only: config.only
])
# Start ExUnit
ExUnit.start()
start_time = System.monotonic_time(:millisecond)
# Load and run test partitions in parallel
tasks = Enum.map(partitions, fn partition ->
Task.async(fn ->
run_partition(partition, config)
end)
end)
# Wait for all partitions to complete
results = Task.await_all(tasks, config.timeout + 30_000) # Extra buffer
end_time = System.monotonic_time(:millisecond)
total_time = end_time - start_time
# Aggregate results
total_tests = Enum.sum(Enum.map(results, & &1.tests))
total_failures = Enum.sum(Enum.map(results, & &1.failures))
# Print summary
IO.puts("\nš Parallel Test Execution Summary")
IO.puts("==================================")
IO.puts(" Total tests: #{total_tests}")
IO.puts(" Failures: #{total_failures}")
IO.puts(" Execution time: #{total_time}ms")
IO.puts(" Average per partition: #{div(total_time, length(partitions))}ms")
if config.profile do
print_performance_profile(total_time, length(partitions), config.max_cases)
end
# Exit with error if there were failures
if total_failures > 0 do
System.halt(1)
end
end
defp discover_test_files do
test_paths = Mix.Project.config()[:test_paths] || ["test"]
test_paths
|> Enum.flat_map(fn path ->
Path.wildcard("#{path}/**/*_test.exs")
end)
|> Enum.sort()
end
defp partition_tests(test_files, :module) do
# Group by estimated execution time and resource usage
test_files
|> Enum.map(&analyze_test_file/1)
|> Enum.sort_by(& &1.estimated_time, :desc) # Longest first
|> distribute_evenly()
end
defp partition_tests(test_files, :file) do
# Simple file-based partitioning
test_files
|> Enum.chunk_every(max(1, div(length(test_files), 4)))
end
defp partition_tests(test_files, :test) do
# Individual test-based partitioning (slowest but most balanced)
test_files
|> Enum.flat_map(&extract_test_cases/1)
|> Enum.chunk_every(10) # Group in batches of 10
end
defp analyze_test_file(file_path) do
# Estimate test execution characteristics
file_size = File.stat!(file_path).size
# Read file to analyze test patterns
content = File.read!(file_path)
# Count test cases
test_count = length(Regex.scan(~r/test\s+"/, content))
# Detect resource-intensive patterns
resource_intensive =
String.contains?(content, ["GenServer", "Task", "Agent", ":integration"]) or
String.contains?(content, ["@tag :slow", "@moduletag :slow"])
# Estimate execution time (heuristic)
estimated_time = cond do
resource_intensive -> test_count * 500 + file_size / 100
test_count > 10 -> test_count * 100 + file_size / 200
true -> test_count * 50 + file_size / 500
end
%{
file: file_path,
test_count: test_count,
file_size: file_size,
estimated_time: trunc(estimated_time),
resource_intensive: resource_intensive
}
end
defp distribute_evenly(analyzed_files) do
# Use bin packing algorithm to distribute tests evenly
num_partitions = min(4, max(1, div(length(analyzed_files), 3)))
partitions = Enum.map(1..num_partitions, fn _ -> [] end)
analyzed_files
|> Enum.reduce(partitions, fn file, partitions ->
# Find partition with least estimated total time
{lightest_partition_idx, _} =
partitions
|> Enum.with_index()
|> Enum.min_by(fn {partition, _} ->
Enum.sum(Enum.map(partition, & &1.estimated_time))
end)
List.update_at(partitions, lightest_partition_idx, &[file | &1])
end)
|> Enum.map(fn partition -> Enum.map(partition, & &1.file) end)
|> Enum.reject(&Enum.empty?/1)
end
defp extract_test_cases(_file_path) do
# For now, return file as single case
# TODO: Parse AST to extract individual test cases
[_file_path]
end
defp run_partition(test_files, config) do
# Load test files
Enum.each(test_files, fn file ->
Code.load_file(file)
end)
# Mock result for now - in real implementation,
# this would run ExUnit on the specific partition
%{
tests: length(test_files) * 5, # Estimate
failures: 0,
partition: test_files
}
end
defp start_profiler do
spawn(fn ->
:erlang.trace(:all, true, [:call, :timestamp])
profiler_loop()
end)
end
defp profiler_loop do
receive
{:stop} -> :ok
msg ->
# Process profiling message
IO.inspect(msg, label: "Profile")
profiler_loop()
end
end
defp stop_profiler(pid) do
send(pid, {:stop})
:erlang.trace(:all, false, [:call])
end
defp print_performance_profile(total_time, partitions, workers) do
IO.puts("\nš Performance Profile")
IO.puts("=====================")
IO.puts(" Theoretical max speedup: #{workers}x")
# Calculate efficiency (simplified)
sequential_estimate = total_time * partitions
actual_parallel = total_time
efficiency = min(100.0, (sequential_estimate / actual_parallel) * 100 / workers)
IO.puts(" Parallel efficiency: #{Float.round(efficiency, 1)}%")
IO.puts(" Time per worker: #{div(total_time, workers)}ms")
end
end