Class: Henitai::ExecutionEngine
- Inherits:
-
Object
- Object
- Henitai::ExecutionEngine
- Includes:
- EnvScope
- Defined in:
- lib/henitai/execution_engine.rb,
lib/henitai/execution_engine/env_scope.rb,
sig/henitai.rbs
Overview
Runs pending mutants through the selected integration.
Defined Under Namespace
Modules: EnvScope
Instance Method Summary collapse
- #execute(mutants, integration, config, progress_reporter) ⇒ Array[Mutant]
- #parallel_execution?(config, mutants) ⇒ Boolean
- #prioritized_tests_for(mutant, integration, config) ⇒ Array[String]
- #process_mutant(mutant, integration, config, progress_reporter, mutex) ⇒ void
- #run(mutants, integration, config, progress_reporter: nil) ⇒ Array[Mutant]
- #run_linear(mutants, integration, config, progress_reporter, mutex) ⇒ void
- #run_parallel(mutants, integration, config, progress_reporter) ⇒ void
-
#run_with_flaky_retry(mutant, integration, config, test_files, mutex) ⇒ ScenarioExecutionResult
Retry logic is kept in one place to preserve the status transition flow.
- #test_history(config) ⇒ Hash[untyped, untyped]
- #test_prioritizer(config) ⇒ TestPrioritizer
- #timing_source(config) ⇒ Object
- #warn_flaky_mutants(total_mutants) ⇒ void
- #with_reports_dir { ... } ⇒ Object
- #with_worker_slot { ... } ⇒ Object
- #worker_count(config) ⇒ Integer
Instance Method Details
#execute(mutants, integration, config, progress_reporter) ⇒ Array[Mutant]
25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/henitai/execution_engine.rb', line 25 def execute(mutants, integration, config, progress_reporter) @flaky_retry_count = 0 pending_mutants = Array(mutants).select(&:pending?) mutex = Mutex.new if parallel_execution?(config, pending_mutants) run_parallel(pending_mutants, integration, config, progress_reporter) else run_linear(pending_mutants, integration, config, progress_reporter, mutex) end warn_flaky_mutants(pending_mutants.size) mutants end |
#parallel_execution?(config, mutants) ⇒ Boolean
39 40 41 |
# File 'lib/henitai/execution_engine.rb', line 39 def parallel_execution?(config, mutants) worker_count(config) > 1 && mutants.size > 1 end |
#prioritized_tests_for(mutant, integration, config) ⇒ Array[String]
102 103 104 105 106 107 108 109 110 |
# File 'lib/henitai/execution_engine.rb', line 102 def prioritized_tests_for(mutant, integration, config) tests = reject_excluded_tests(integration.select_tests(mutant.subject), config) tests = per_test_coverage_selector.filter( tests, mutant, reports_dir: config.reports_dir ) test_prioritizer(config).sort(tests, mutant, test_history(config)) end |
#process_mutant(mutant, integration, config, progress_reporter, mutex) ⇒ void
This method returns an undefined value.
73 74 75 76 77 78 79 80 81 82 |
# File 'lib/henitai/execution_engine.rb', line 73 def process_mutant(mutant, integration, config, progress_reporter, mutex) test_files = prioritized_tests_for(mutant, integration, config) record_test_files(mutant, test_files) return record_no_coverage(mutant, progress_reporter, mutex) if test_files.empty? scenario_result = run_with_flaky_retry(mutant, integration, config, test_files, mutex) mutant.status = scenario_status(scenario_result) report_progress(mutant, scenario_result, progress_reporter, mutex) end |
#run(mutants, integration, config, progress_reporter: nil) ⇒ Array[Mutant]
11 12 13 14 15 16 17 18 19 20 21 |
# File 'lib/henitai/execution_engine.rb', line 11 def run(mutants, integration, config, progress_reporter: nil) with_reports_dir(config) do with_coverage_dir(config) do with_max_log_bytes(config) do with_worker_slot do execute(mutants, integration, config, progress_reporter) end end end end end |
#run_linear(mutants, integration, config, progress_reporter, mutex) ⇒ void
This method returns an undefined value.
53 54 55 56 57 |
# File 'lib/henitai/execution_engine.rb', line 53 def run_linear(mutants, integration, config, progress_reporter, mutex) mutants.each do |mutant| process_mutant(mutant, integration, config, progress_reporter, mutex) end end |
#run_parallel(mutants, integration, config, progress_reporter) ⇒ void
This method returns an undefined value.
59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/henitai/execution_engine.rb', line 59 def run_parallel(mutants, integration, config, progress_reporter) runner = ProcessWorkerRunner.new(worker_count: worker_count(config)) results = runner.run( mutants, integration, config, progress_reporter, test_file_resolver: ->(mutant) { prioritized_tests_for(mutant, integration, config) }, timeout_resolver: ->(_mutant, test_files) { resolved_timeout(test_files, config) } ) @flaky_retry_count = runner.flaky_retry_count results end |
#run_with_flaky_retry(mutant, integration, config, test_files, mutex) ⇒ ScenarioExecutionResult
Retry logic is kept in one place to preserve the status transition flow. The retry budget is configurable because repeated survivors can multiply runtime on real CI workloads. rubocop:disable Metrics/MethodLength
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/henitai/execution_engine.rb', line 191 def run_with_flaky_retry(mutant, integration, config, test_files, mutex) timeout = resolved_timeout(test_files, config) scenario_result = integration.run_mutant( mutant:, test_files:, timeout: timeout ) return scenario_result unless scenario_status(scenario_result) == :survived retries = 0 max_flaky_retries(config).times do retries += 1 scenario_result = integration.run_mutant( mutant:, test_files:, timeout: timeout ) break unless scenario_status(scenario_result) == :survived end mutex.synchronize { @flaky_retry_count += 1 } if retries.positive? scenario_result end |
#test_history(config) ⇒ Hash[untyped, untyped]
181 182 183 184 185 |
# File 'lib/henitai/execution_engine.rb', line 181 def test_history(config) return {} unless config.respond_to?(:history) config.history || {} end |
#test_prioritizer(config) ⇒ TestPrioritizer
127 128 129 |
# File 'lib/henitai/execution_engine.rb', line 127 def test_prioritizer(config) @test_prioritizer ||= TestPrioritizer.new(timing_source: timing_source(config)) end |
#timing_source(config) ⇒ Object
131 132 133 134 |
# File 'lib/henitai/execution_engine.rb', line 131 def timing_source(config) path = File.join(config.reports_dir, PerTestCoverageCollector::REPORT_FILE_NAME) -> { CoverageReportReader.new.durations_by_test(path) } end |
#warn_flaky_mutants(total_mutants) ⇒ void
This method returns an undefined value.
222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/henitai/execution_engine.rb', line 222 def warn_flaky_mutants(total_mutants) return if total_mutants.zero? flaky_ratio = @flaky_retry_count.to_f / total_mutants return unless flaky_ratio > 0.05 warn format( "Flaky-test mitigation: %<flaky>d/%<total>d mutants required retries (%<ratio>.2f%%)", flaky: @flaky_retry_count, total: total_mutants, ratio: flaky_ratio * 100.0 ) end |
#with_reports_dir { ... } ⇒ Object
762 |
# File 'sig/henitai.rbs', line 762
def with_reports_dir: (untyped) { () -> untyped } -> untyped
|
#with_worker_slot { ... } ⇒ Object
750 |
# File 'sig/henitai.rbs', line 750
def with_worker_slot: () { () -> untyped } -> untyped
|
#worker_count(config) ⇒ Integer
43 44 45 46 47 48 49 50 51 |
# File 'lib/henitai/execution_engine.rb', line 43 def worker_count(config) configured_jobs = config.respond_to?(:jobs) ? config.jobs : nil return configured_jobs if configured_jobs # The fallback stays conservative for now; the execution policy still # defaults to a single worker even though AvailableCpuCount exists as a # future policy hook. 1 end |