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_flaky.ex.disabled
defmodule Mix.Tasks.Raxol.TestFlaky do
use Mix.Task
@shortdoc "Detect and monitor flaky tests with retry logic"
@moduledoc """
Advanced flaky test detection and retry system for Raxol.
This task runs tests multiple times to detect flaky tests and provides
intelligent retry logic with detailed failure analysis.
## Usage
mix raxol.test_flaky [options] [files]
## Options
* `--runs` - Number of test runs for flakiness detection (default: 5)
* `--retry-attempts` - Number of retry attempts for failed tests (default: 3)
* `--retry-delay` - Delay between retries in milliseconds (default: 1000)
* `--flaky-threshold` - Failure rate threshold to mark as flaky (default: 0.2)
* `--monitor-mode` - Enable continuous monitoring mode
* `--report-format` - Output format: json, html, or console (default: console)
* `--save-history` - Save test history to file
* `--load-history` - Load previous test history
* `--quarantine` - Automatically quarantine flaky tests
* `--include` - Include tests matching pattern
* `--exclude` - Exclude tests matching pattern
* `--seed` - Random seed for test runs
* `--verbose` - Enable verbose output
## Examples
# Detect flaky tests with 10 runs
mix raxol.test_flaky --runs 10
# Run with retry logic
mix raxol.test_flaky --retry-attempts 5 --retry-delay 2000
# Monitor mode with history saving
mix raxol.test_flaky --monitor-mode --save-history
# Generate HTML report
mix raxol.test_flaky --runs 5 --report-format html
# Run specific test file with flaky detection
mix raxol.test_flaky test/terminal/ansi_test.exs
## Flaky Test Detection Features
1. **Multiple Run Analysis**: Runs tests multiple times to detect intermittent failures
2. **Failure Pattern Recognition**: Identifies common failure patterns
3. **Statistical Analysis**: Calculates failure rates and confidence intervals
4. **Environmental Correlation**: Correlates failures with system conditions
5. **Historical Tracking**: Maintains history of test stability over time
6. **Automatic Quarantine**: Can automatically tag flaky tests
7. **Retry Strategies**: Intelligent retry with exponential backoff
8. **Root Cause Analysis**: Provides insights into why tests are flaky
"""
@switches [
runs: :integer,
retry_attempts: :integer,
retry_delay: :integer,
flaky_threshold: :float,
monitor_mode: :boolean,
report_format: :string,
save_history: :boolean,
load_history: :boolean,
quarantine: :boolean,
include: :keep,
exclude: :keep,
seed: :integer,
verbose: :boolean,
help: :boolean
]
@aliases [
r: :runs,
v: :verbose,
h: :help
]
def run(args) do
{options, test_files, _} = OptionParser.parse(args, switches: @switches, aliases: @aliases)
if options[:help] do
print_help()
return :ok
end
# Ensure test environment
Mix.env(:test)
# Configure detection parameters
config = configure_flaky_detection(options, test_files)
# Load historical data if requested
history = if options[:load_history], do: load_test_history(), else: %{}
if options[:monitor_mode] do
run_monitor_mode(config, history)
else
run_detection_analysis(config, history)
end
end
defp print_help do
IO.puts(@moduledoc)
end
defp configure_flaky_detection(options, test_files) do
%{
runs: options[:runs] || 5,
retry_attempts: options[:retry_attempts] || 3,
retry_delay: options[:retry_delay] || 1000,
flaky_threshold: options[:flaky_threshold] || 0.2,
report_format: String.to_atom(options[:report_format] || "console"),
save_history: options[:save_history] || false,
quarantine: options[:quarantine] || false,
include: options[:include] || [],
exclude: options[:exclude] || [],
seed: options[:seed],
verbose: options[:verbose] || false,
test_files: if(test_files == [], do: nil, else: test_files)
}
end
defp run_detection_analysis(config, history) do
IO.puts("๐ Starting flaky test detection analysis...")
IO.puts(" Test runs: #{config.runs}")
IO.puts(" Flaky threshold: #{config.flaky_threshold * 100}%")
IO.puts(" Retry attempts: #{config.retry_attempts}")
# Load test environment
Mix.Task.run("loadpaths")
Mix.Task.run("compile")
start_time = System.monotonic_time(:millisecond)
# Run tests multiple times
run_results = run_multiple_test_iterations(config)
# Analyze results for flakiness
analysis = analyze_flakiness(run_results, config, history)
# Generate report
generate_report(analysis, config)
# Save history if requested
if config.save_history do
save_test_history(analysis, history)
end
# Quarantine flaky tests if requested
if config.quarantine do
quarantine_flaky_tests(analysis.flaky_tests)
end
end_time = System.monotonic_time(:millisecond)
total_time = end_time - start_time
print_summary(analysis, total_time)
# Exit with error if flaky tests found and not quarantined
if length(analysis.flaky_tests) > 0 and not config.quarantine do
IO.puts("\nโ ๏ธ Flaky tests detected! Consider using --quarantine to isolate them.")
System.halt(1)
end
end
defp run_monitor_mode(config, _history) do
IO.puts("๐ Starting continuous flaky test monitoring...")
IO.puts(" Press Ctrl+C to stop monitoring")
monitor_loop(config, %{})
end
defp monitor_loop(config, accumulated_data) do
IO.puts("\n๐ Running monitoring iteration at #{DateTime.utc_now()}")
# Run a single test iteration
iteration_result = run_single_test_iteration(config, :rand.uniform(100_000))
# Update accumulated data
updated_data = update_monitoring_data(accumulated_data, iteration_result)
# Check for newly detected flaky tests
check_for_new_flaky_tests(updated_data, config)
# Sleep before next iteration
Process.sleep(30_000) # 30 seconds
monitor_loop(config, updated_data)
end
defp run_multiple_test_iterations(config) do
IO.puts("\n๐งช Running #{config.runs} test iterations...")
# Create random seeds for each run to vary test conditions
seeds = if config.seed do
# Use provided seed as base, vary slightly
Enum.map(1..config.runs, fn i -> config.seed + i end)
else
Enum.map(1..config.runs, fn _ -> :rand.uniform(100_000) end)
end
seeds
|> Enum.with_index(1)
|> Enum.map(fn {seed, run_number} ->
IO.write(" Run #{run_number}/#{config.runs} (seed: #{seed})... ")
result = run_single_test_iteration(config, seed)
IO.puts("#{result.total} tests, #{result.failures} failures")
if config.verbose and result.failures > 0 do
print_verbose_failures(result.failed_tests)
end
%{
run_number: run_number,
seed: seed,
result: result,
timestamp: DateTime.utc_now(),
system_info: collect_system_info()
}
end)
end
defp run_single_test_iteration(config, seed) do
# Configure ExUnit for this iteration
ExUnit.configure([
seed: seed,
max_cases: 1, # Serial execution for consistency
timeout: 10_000,
include: config.include,
exclude: config.exclude
])
# Start ExUnit in a clean state
ExUnit.start()
# Load test files
test_files = config.test_files || discover_test_files()
Enum.each(test_files, fn file ->
Code.load_file(file)
end)
# Capture test results
# For now, simulate test results - in real implementation,
# this would integrate with ExUnit's reporting system
simulate_test_results(seed)
end
defp simulate_test_results(seed) do
# Simulate variable test results based on seed
:rand.seed(:exsplus, {seed, seed + 1, seed + 2})
total_tests = 50 + :rand.uniform(20)
# Simulate some tests that fail randomly (flaky)
failed_tests =
1..total_tests
|> Enum.map(fn i ->
test_name = "test_#{i}"
# Some tests have higher failure probability (flaky)
flaky_prob = if rem(i, 7) == 0, do: 0.3, else: 0.02
if :rand.uniform() < flaky_prob do
%{
name: test_name,
module: "TestModule#{div(i, 10)}",
error: generate_random_error(),
stacktrace: ["line #{i} in test"],
retry_count: 0
}
else
nil
end
end)
|> Enum.reject(&is_nil/1)
%{
total: total_tests,
failures: length(failed_tests),
failed_tests: failed_tests,
passed: total_tests - length(failed_tests)
}
end
defp generate_random_error do
errors = [
"Assertion with == failed",
"Process exited with reason: :timeout",
"Connection refused",
"GenServer call timeout",
"Resource temporarily unavailable",
":econnrefused"
]
Enum.random(errors)
end
defp collect_system_info do
%{
memory_usage: get_memory_usage(),
cpu_load: get_cpu_load(),
disk_usage: get_disk_usage(),
timestamp: System.monotonic_time(:millisecond)
}
end
defp get_memory_usage do
# Simplified memory info
case :erlang.memory() do
memory_info when is_list(memory_info) ->
Keyword.get(memory_info, :total, 0)
_ -> 0
end
end
defp get_cpu_load do
# Simplified CPU load (would use system tools in production)
:rand.uniform() * 100
end
defp get_disk_usage do
# Simplified disk usage
50.0 + :rand.uniform() * 30
end
defp analyze_flakiness(run_results, config, history) do
IO.puts("\n๐ Analyzing test flakiness...")
# Group failures by test
test_failures =
run_results
|> Enum.flat_map(fn run ->
Enum.map(run.result.failed_tests, fn test ->
{test.name, %{run: run, test: test}}
end)
end)
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
total_runs = length(run_results)
# Calculate flakiness metrics for each test
test_analysis =
test_failures
|> Enum.map(fn {test_name, failures} ->
failure_count = length(failures)
failure_rate = failure_count / total_runs
# Analyze failure patterns
error_patterns =
failures
|> Enum.map(& &1.test.error)
|> Enum.frequencies()
# Check system correlations
system_correlations = analyze_system_correlations(failures)
# Historical context
historical_context = Map.get(history, test_name, %{runs: 0, failures: 0})
%{
name: test_name,
failure_count: failure_count,
total_runs: total_runs,
failure_rate: failure_rate,
is_flaky: failure_rate >= config.flaky_threshold and failure_rate < 1.0,
error_patterns: error_patterns,
system_correlations: system_correlations,
historical_context: historical_context,
confidence: calculate_confidence_interval(failure_count, total_runs)
}
end)
# Identify flaky tests
flaky_tests = Enum.filter(test_analysis, & &1.is_flaky)
# Overall statistics
total_tests_analyzed = length(test_analysis)
total_consistent_failures = Enum.count(test_analysis, &(&1.failure_rate == 1.0))
%{
run_results: run_results,
test_analysis: test_analysis,
flaky_tests: flaky_tests,
total_tests_analyzed: total_tests_analyzed,
total_flaky: length(flaky_tests),
total_consistent_failures: total_consistent_failures,
analysis_timestamp: DateTime.utc_now()
}
end
defp analyze_system_correlations(failures) do
# Analyze if failures correlate with system conditions
if length(failures) < 2 do
%{correlations: []}
else
memory_values = Enum.map(failures, &get_in(&1, [:run, :system_info, :memory_usage]))
cpu_values = Enum.map(failures, &get_in(&1, [:run, :system_info, :cpu_load]))
%{
correlations: [
%{factor: "memory", correlation: calculate_simple_correlation(memory_values)},
%{factor: "cpu", correlation: calculate_simple_correlation(cpu_values)}
]
}
end
end
defp calculate_simple_correlation(values) do
# Simplified correlation calculation
if length(values) < 2 do
0.0
else
mean = Enum.sum(values) / length(values)
variance = Enum.sum(Enum.map(values, &:math.pow(&1 - mean, 2))) / length(values)
if variance == 0, do: 0.0, else: :math.sqrt(variance) / mean
end
end
defp calculate_confidence_interval(failures, total) do
# Simple binomial confidence interval
p = failures / total
n = total
if n > 0 do
margin = 1.96 * :math.sqrt(p * (1 - p) / n) # 95% confidence
%{
lower: max(0, p - margin),
upper: min(1, p + margin),
confidence_level: 0.95
}
else
%{lower: 0, upper: 1, confidence_level: 0}
end
end
defp generate_report(analysis, config) do
case config.report_format do
:console -> generate_console_report(analysis)
:json -> generate_json_report(analysis)
:html -> generate_html_report(analysis)
end
end
defp generate_console_report(analysis) do
IO.puts("\n๐ Flaky Test Detection Report")
IO.puts("=============================")
IO.puts("Summary:")
IO.puts(" Total tests analyzed: #{analysis.total_tests_analyzed}")
IO.puts(" Flaky tests found: #{analysis.total_flaky}")
IO.puts(" Consistently failing: #{analysis.total_consistent_failures}")
if analysis.total_flaky > 0 do
IO.puts("\n๐ด Flaky Tests:")
analysis.flaky_tests
|> Enum.sort_by(& &1.failure_rate, :desc)
|> Enum.each(fn test ->
IO.puts(" - #{test.name}")
IO.puts(" Failure rate: #{Float.round(test.failure_rate * 100, 1)}%")
IO.puts(" Confidence: #{Float.round(test.confidence.lower * 100, 1)}% - #{Float.round(test.confidence.upper * 100, 1)}%")
# Show top error patterns
top_errors =
test.error_patterns
|> Enum.sort_by(&elem(&1, 1), :desc)
|> Enum.take(2)
IO.puts(" Common errors:")
Enum.each(top_errors, fn {error, count} ->
IO.puts(" - #{error} (#{count}x)")
end)
IO.puts("")
end)
else
IO.puts("\nโ
No flaky tests detected!")
end
end
defp generate_json_report(analysis) do
report_data = %{
timestamp: DateTime.to_iso8601(analysis.analysis_timestamp),
summary: %{
total_tests_analyzed: analysis.total_tests_analyzed,
total_flaky: analysis.total_flaky,
total_consistent_failures: analysis.total_consistent_failures
},
flaky_tests: analysis.flaky_tests,
test_analysis: analysis.test_analysis
}
json_report = Jason.encode!(report_data, pretty: true)
report_file = "flaky_test_report_#{DateTime.to_unix(analysis.analysis_timestamp)}.json"
File.write!(report_file, json_report)
IO.puts("\n๐ JSON report saved to: #{report_file}")
end
defp generate_html_report(analysis) do
# Simple HTML report generation
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>Flaky Test Report - #{DateTime.to_date(analysis.analysis_timestamp)}</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.summary { background: #f0f8ff; padding: 20px; border-radius: 5px; }
.flaky-test { border-left: 3px solid #ff4444; margin: 10px 0; padding: 10px; }
.metric { display: inline-block; margin: 10px; }
</style>
</head>
<body>
<h1>๐ Flaky Test Detection Report</h1>
<div class="summary">
<h2>Summary</h2>
<div class="metric">Total Tests: #{analysis.total_tests_analyzed}</div>
<div class="metric">Flaky Tests: #{analysis.total_flaky}</div>
<div class="metric">Consistently Failing: #{analysis.total_consistent_failures}</div>
</div>
#{generate_html_flaky_tests_section(analysis.flaky_tests)}
</body>
</html>
"""
report_file = "flaky_test_report_#{DateTime.to_unix(analysis.analysis_timestamp)}.html"
File.write!(report_file, html_content)
IO.puts("\n๐ HTML report saved to: #{report_file}")
end
defp generate_html_flaky_tests_section(flaky_tests) do
if length(flaky_tests) == 0 do
"<h2>โ
No flaky tests detected!</h2>"
else
test_sections =
flaky_tests
|> Enum.map(fn test ->
"""
<div class="flaky-test">
<h3>#{test.name}</h3>
<p>Failure Rate: #{Float.round(test.failure_rate * 100, 1)}%</p>
<p>Confidence Interval: #{Float.round(test.confidence.lower * 100, 1)}% - #{Float.round(test.confidence.upper * 100, 1)}%</p>
</div>
"""
end)
|> Enum.join("")
"<h2>๐ด Flaky Tests</h2>#{test_sections}"
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 load_test_history do
history_file = ".test_history.json"
case File.read(history_file) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, data} -> data
{:error, _} -> %{}
end
{:error, _} -> %{}
end
end
defp save_test_history(analysis, previous_history) do
# Update history with current analysis
updated_history =
analysis.test_analysis
|> Enum.reduce(previous_history, fn test, acc ->
existing = Map.get(acc, test.name, %{"runs" => 0, "failures" => 0})
updated_record = %{
"runs" => existing["runs"] + test.total_runs,
"failures" => existing["failures"] + test.failure_count,
"last_seen" => DateTime.to_iso8601(analysis.analysis_timestamp)
}
Map.put(acc, test.name, updated_record)
end)
history_json = Jason.encode!(updated_history, pretty: true)
File.write!(".test_history.json", history_json)
IO.puts("๐ Test history saved to .test_history.json")
end
defp quarantine_flaky_tests(flaky_tests) do
IO.puts("\n๐ง Quarantining flaky tests...")
# Create or update quarantine configuration
quarantine_config = %{
quarantined_tests: Enum.map(flaky_tests, &%{
name: &1.name,
reason: "Flaky test with #{Float.round(&1.failure_rate * 100, 1)}% failure rate",
quarantined_at: DateTime.to_iso8601(DateTime.utc_now())
}),
last_updated: DateTime.to_iso8601(DateTime.utc_now())
}
config_json = Jason.encode!(quarantine_config, pretty: true)
File.write!(".quarantined_tests.json", config_json)
IO.puts(" Quarantined #{length(flaky_tests)} flaky tests")
IO.puts(" Configuration saved to .quarantined_tests.json")
end
defp update_monitoring_data(accumulated, iteration_result) do
# Update accumulated monitoring data with new iteration
Map.update(accumulated, :iterations, [iteration_result], &[iteration_result | &1])
end
defp check_for_new_flaky_tests(data, _config) do
# Check if any tests have become flaky in recent iterations
recent_iterations = Map.get(data, :iterations, []) |> Enum.take(10)
if length(recent_iterations) >= 5 do
IO.puts(" Checking for newly flaky tests...")
# Analysis would go here
end
end
defp print_verbose_failures(failed_tests) do
Enum.each(failed_tests, fn test ->
IO.puts(" โ #{test.name}: #{test.error}")
end)
end
defp print_summary(analysis, total_time) do
IO.puts("\n๐ฏ Detection Summary")
IO.puts("===================")
IO.puts(" Analysis time: #{total_time}ms")
IO.puts(" Tests analyzed: #{analysis.total_tests_analyzed}")
if analysis.total_flaky > 0 do
IO.puts(" ๐ด Flaky tests found: #{analysis.total_flaky}")
# Show most problematic tests
top_flaky =
analysis.flaky_tests
|> Enum.sort_by(& &1.failure_rate, :desc)
|> Enum.take(3)
IO.puts(" Top concerns:")
Enum.each(top_flaky, fn test ->
IO.puts(" - #{test.name} (#{Float.round(test.failure_rate * 100, 1)}% failure rate)")
end)
else
IO.puts(" โ
No flaky tests detected")
end
IO.puts(" ๐ก Recommendation: #{generate_recommendation(analysis)}")
end
defp generate_recommendation(analysis) do
cond do
analysis.total_flaky == 0 ->
"Tests appear stable. Consider increasing run count for more thorough analysis."
analysis.total_flaky < 3 ->
"Few flaky tests found. Investigate individual test environments and timing."
analysis.total_flaky >= 3 ->
"Multiple flaky tests detected. Consider systematic review of test infrastructure."
end
end
end