Class: Henitai::ExecutionEngine

Inherits:
Object
  • Object
show all
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

Instance Method Details

#execute(mutants, integration, config, progress_reporter) ⇒ Array[Mutant]

Parameters:

  • (Array[Mutant])
  • (Object)
  • (Object)
  • (Object, nil)

Returns:



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

Parameters:

Returns:

  • (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]

Parameters:

Returns:

  • (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.

Parameters:

  • (Mutant)
  • (Object)
  • (Object)
  • (Object, nil)
  • (Object)


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]

Parameters:

  • (Array[Mutant])
  • (Object)
  • (Object)
  • progress_reporter: (Object) (defaults to: nil)

Returns:



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.

Parameters:

  • (Array[Mutant])
  • (Object)
  • (Object)
  • (Object, nil)
  • (Object)


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.

Parameters:

  • (Array[Mutant])
  • (Object)
  • (Object)
  • (Object, nil)


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

Parameters:

  • (Mutant)
  • (Object)
  • (Object)
  • (Array[String])
  • (Object)

Returns:



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]

Parameters:

  • (Object)

Returns:

  • (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

Parameters:

  • (Object)

Returns:



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

Parameters:

  • (Object)

Returns:

  • (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.

Parameters:

  • (Integer)


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

Parameters:

  • (Object)

Yields:

Yield Returns:

  • (Object)

Returns:

  • (Object)


764
# File 'sig/henitai.rbs', line 764

def with_reports_dir: (untyped) { () -> untyped } -> untyped

#with_worker_slot { ... } ⇒ Object

Yields:

Yield Returns:

  • (Object)

Returns:

  • (Object)


752
# File 'sig/henitai.rbs', line 752

def with_worker_slot: () { () -> untyped } -> untyped

#worker_count(config) ⇒ Integer

Parameters:

  • (Object)

Returns:

  • (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