henitai 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -1
  3. data/README.md +3 -1
  4. data/assets/schema/henitai.schema.json +0 -4
  5. data/lib/henitai/arid_node_filter.rb +3 -0
  6. data/lib/henitai/available_cpu_count.rb +79 -0
  7. data/lib/henitai/cli.rb +7 -4
  8. data/lib/henitai/configuration.rb +3 -5
  9. data/lib/henitai/configuration_validator.rb +1 -11
  10. data/lib/henitai/coverage_bootstrapper.rb +112 -8
  11. data/lib/henitai/coverage_formatter.rb +4 -4
  12. data/lib/henitai/coverage_report_reader.rb +67 -0
  13. data/lib/henitai/eager_load.rb +11 -0
  14. data/lib/henitai/equivalence_detector.rb +60 -1
  15. data/lib/henitai/execution_engine.rb +34 -22
  16. data/lib/henitai/integration/rspec_process_runner.rb +58 -0
  17. data/lib/henitai/integration.rb +222 -100
  18. data/lib/henitai/minitest_simplecov.rb +3 -0
  19. data/lib/henitai/mutant/activator.rb +78 -42
  20. data/lib/henitai/mutant.rb +3 -1
  21. data/lib/henitai/mutant_generator.rb +25 -48
  22. data/lib/henitai/operator.rb +6 -1
  23. data/lib/henitai/operators/assignment_expression.rb +7 -23
  24. data/lib/henitai/operators/conditional_expression.rb +1 -7
  25. data/lib/henitai/operators/method_chain_unwrap.rb +41 -0
  26. data/lib/henitai/operators/regex_mutator.rb +89 -0
  27. data/lib/henitai/operators/unary_operator.rb +36 -0
  28. data/lib/henitai/operators/update_operator.rb +70 -0
  29. data/lib/henitai/operators.rb +4 -0
  30. data/lib/henitai/parallel_execution_runner.rb +135 -0
  31. data/lib/henitai/per_test_coverage_selector.rb +60 -0
  32. data/lib/henitai/result.rb +16 -4
  33. data/lib/henitai/rspec_coverage_formatter.rb +10 -0
  34. data/lib/henitai/runner.rb +75 -11
  35. data/lib/henitai/source_parser.rb +12 -1
  36. data/lib/henitai/static_filter.rb +53 -38
  37. data/lib/henitai/version.rb +1 -1
  38. data/lib/henitai.rb +3 -0
  39. data/sig/henitai.rbs +65 -11
  40. metadata +18 -4
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ # Runs pending mutants across worker threads with signal and stdin handling.
5
+ class ParallelExecutionRunner
6
+ ParallelExecutionContext = Struct.new(
7
+ :queue, :integration, :config, :progress_reporter,
8
+ :mutex, :state, :old_handlers, :stdin_watcher
9
+ )
10
+
11
+ def initialize(worker_count:)
12
+ @worker_count = worker_count
13
+ end
14
+
15
+ def run(mutants, integration, config, progress_reporter, options = {})
16
+ context = build_parallel_context(
17
+ mutants,
18
+ integration,
19
+ config,
20
+ progress_reporter
21
+ )
22
+ execute_parallel_execution(
23
+ context,
24
+ stdin_pipe: options.fetch(:stdin_pipe, false),
25
+ process_mutant: options.fetch(:process_mutant)
26
+ )
27
+ end
28
+
29
+ def execute_parallel_execution(context, stdin_pipe:, process_mutant:)
30
+ install_parallel_signal_traps(context)
31
+ start_parallel_stdin_watcher(context, stdin_pipe)
32
+ parallel_workers(context, process_mutant).each(&:join)
33
+ ensure
34
+ stop_parallel_stdin_watcher(context)
35
+ restore_parallel_signal_traps(context)
36
+ raise context.state[:error] if context&.state&.fetch(:error, nil)
37
+ raise Interrupt if context&.state&.fetch(:stopping, false)
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :worker_count
43
+
44
+ def build_parallel_queue(mutants)
45
+ Queue.new.tap { |queue| mutants.each { |mutant| queue << mutant } }
46
+ end
47
+
48
+ def build_parallel_context(mutants, integration, config, progress_reporter)
49
+ ParallelExecutionContext.new(
50
+ build_parallel_queue(mutants),
51
+ integration,
52
+ config,
53
+ progress_reporter,
54
+ Mutex.new,
55
+ { stopping: false }
56
+ )
57
+ end
58
+
59
+ def install_parallel_signal_traps(context)
60
+ context.old_handlers = {
61
+ int: trap(:INT) { stop_parallel_execution(context) },
62
+ term: trap(:TERM) { stop_parallel_execution(context) },
63
+ hup: trap(:HUP) { stop_parallel_execution(context) }
64
+ }
65
+ end
66
+
67
+ def stop_parallel_execution(context)
68
+ context.state[:stopping] = true
69
+ context.queue.clear
70
+ end
71
+
72
+ def start_parallel_stdin_watcher(context, stdin_pipe)
73
+ return unless stdin_pipe
74
+ # CI runners expose stdin as a non-interactive pipe, so EOF there should
75
+ # not be treated as a user disconnect.
76
+ return if ci_environment?
77
+
78
+ context.stdin_watcher = Thread.new do
79
+ $stdin.read
80
+ stop_parallel_execution(context)
81
+ rescue IOError, Errno::EBADF
82
+ nil
83
+ end
84
+ end
85
+
86
+ def parallel_workers(context, process_mutant)
87
+ Array.new(worker_count) { Thread.new { process_parallel_worker(context, process_mutant) } }
88
+ end
89
+
90
+ def process_parallel_worker(context, process_mutant)
91
+ loop do
92
+ break if context.state[:stopping]
93
+
94
+ process_mutant.call(
95
+ context.queue.pop(true),
96
+ context.integration,
97
+ context.config,
98
+ context.progress_reporter,
99
+ context.mutex
100
+ )
101
+ rescue ThreadError
102
+ break
103
+ rescue StandardError => e
104
+ record_parallel_error(context, e)
105
+ break
106
+ end
107
+ end
108
+
109
+ def stop_parallel_stdin_watcher(context)
110
+ context&.stdin_watcher&.kill
111
+ end
112
+
113
+ def restore_parallel_signal_traps(context)
114
+ handlers = context&.old_handlers
115
+ return unless handlers
116
+
117
+ trap(:INT, handlers[:int] || "DEFAULT")
118
+ trap(:TERM, handlers[:term] || "DEFAULT")
119
+ trap(:HUP, handlers[:hup] || "DEFAULT")
120
+ end
121
+
122
+ def record_parallel_error(context, error)
123
+ context.mutex.synchronize do
124
+ context.state[:error] ||= error
125
+ context.state[:stopping] = true
126
+ context.queue.clear
127
+ end
128
+ end
129
+
130
+ def ci_environment?
131
+ value = ENV.fetch("CI", nil)
132
+ value && !%w[0 false].include?(value.downcase)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ # Narrows candidate test files using the per-test coverage report.
5
+ class PerTestCoverageSelector
6
+ def initialize(coverage_report_reader: CoverageReportReader.new)
7
+ @coverage_report_reader = coverage_report_reader
8
+ end
9
+
10
+ def filter(tests, mutant, reports_dir:)
11
+ candidates = Array(tests)
12
+ return candidates if candidates.empty?
13
+ return candidates unless location_available?(mutant)
14
+ return candidates unless per_test_coverage_available?(reports_dir)
15
+
16
+ covered_tests = candidates.select do |test|
17
+ covers_mutant?(test, mutant, reports_dir)
18
+ end
19
+ covered_tests.empty? ? candidates : covered_tests
20
+ end
21
+
22
+ private
23
+
24
+ def location_available?(mutant)
25
+ mutant.respond_to?(:location) &&
26
+ mutant.location.is_a?(Hash) &&
27
+ mutant.location[:file] &&
28
+ mutant.location[:start_line] &&
29
+ mutant.location[:end_line]
30
+ end
31
+
32
+ def covers_mutant?(test, mutant, reports_dir)
33
+ covered_lines = coverage_lines_for(test, mutant, reports_dir)
34
+ mutant_lines(mutant).any? { |line| covered_lines.include?(line) }
35
+ end
36
+
37
+ def coverage_lines_for(test, mutant, reports_dir)
38
+ source_map = per_test_coverage(reports_dir)[test.to_s] || {}
39
+ Array(source_map[File.expand_path(mutant.location[:file])]).uniq
40
+ end
41
+
42
+ def mutant_lines(mutant)
43
+ (mutant.location[:start_line]..mutant.location[:end_line]).to_a
44
+ end
45
+
46
+ def per_test_coverage(reports_dir)
47
+ @per_test_coverage ||= {}
48
+ @per_test_coverage[reports_dir] ||= begin
49
+ path = File.join(reports_dir, "henitai_per_test.json")
50
+ coverage_report_reader.test_lines_by_file(path)
51
+ end
52
+ end
53
+
54
+ def per_test_coverage_available?(reports_dir)
55
+ !per_test_coverage(reports_dir).empty?
56
+ end
57
+
58
+ attr_reader :coverage_report_reader
59
+ end
60
+ end
@@ -11,13 +11,15 @@ module Henitai
11
11
  include UnparseHelper
12
12
 
13
13
  SCHEMA_VERSION = "1.0"
14
+ DEFAULT_THRESHOLDS = { high: 80, low: 60 }.freeze
14
15
 
15
- attr_reader :mutants, :started_at, :finished_at
16
+ attr_reader :mutants, :started_at, :finished_at, :thresholds
16
17
 
17
- def initialize(mutants:, started_at:, finished_at:)
18
+ def initialize(mutants:, started_at:, finished_at:, thresholds: nil)
18
19
  @mutants = mutants
19
20
  @started_at = started_at
20
21
  @finished_at = finished_at
22
+ @thresholds = DEFAULT_THRESHOLDS.merge(thresholds || {})
21
23
  end
22
24
 
23
25
  # @return [Integer] number of killed mutants
@@ -88,7 +90,7 @@ module Henitai
88
90
  def to_stryker_schema
89
91
  {
90
92
  schemaVersion: SCHEMA_VERSION,
91
- thresholds: { high: 80, low: 60 },
93
+ thresholds: thresholds,
92
94
  files: build_files_section
93
95
  }
94
96
  end
@@ -119,7 +121,17 @@ module Henitai
119
121
  status: stryker_status(mutant.status),
120
122
  description: mutant.description,
121
123
  duration: duration_for(mutant)
122
- }.compact
124
+ }.compact.merge(coverage_schema(mutant))
125
+ end
126
+
127
+ def coverage_schema(mutant)
128
+ covered_by = Array(mutant.covered_by).compact
129
+ return {} if covered_by.empty?
130
+
131
+ {
132
+ coveredBy: covered_by,
133
+ testsCompleted: mutant.tests_completed || covered_by.size
134
+ }
123
135
  end
124
136
 
125
137
  def replacement_for(mutant)
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "henitai/coverage_formatter"
5
+
6
+ RSpec::Core::Formatters.register(
7
+ Henitai::CoverageFormatter,
8
+ :example_finished,
9
+ :dump_summary
10
+ )
@@ -35,18 +35,24 @@ module Henitai
35
35
  end
36
36
 
37
37
  # Entry point — runs the full pipeline and returns a Result.
38
+ #
39
+ # Coverage bootstrap (Gate 0) runs in a background thread so that Gate 1
40
+ # (subject resolution) and Gate 2 (mutant generation) proceed concurrently.
41
+ # The thread is joined before Gate 3 (static filtering), which is the first
42
+ # phase that requires coverage data.
43
+ #
44
+ # For targeted runs (`subjects:` provided), the bootstrap is further scoped
45
+ # to the spec files that cover the requested subjects rather than the full
46
+ # suite, reducing the baseline run time proportionally.
47
+ #
38
48
  # @return [Result]
39
49
  def run
40
50
  started_at = Time.now
41
51
  source_files = self.source_files
42
- bootstrap_coverage(source_files)
43
52
  subjects = resolve_subjects(source_files)
44
- mutants = generate_mutants(subjects)
45
- mutants = filter_mutants(mutants)
46
- mutants = execute_mutants(mutants)
47
- finished_at = Time.now
53
+ mutants = execute_mutants(mutants_for(subjects, source_files))
48
54
 
49
- build_result(mutants, started_at, finished_at)
55
+ build_result(mutants, started_at, Time.now)
50
56
  end
51
57
 
52
58
  private
@@ -69,6 +75,29 @@ module Henitai
69
75
  static_filter.apply(mutants, config)
70
76
  end
71
77
 
78
+ def mutants_for(subjects, source_files)
79
+ bootstrap_thread = bootstrap_mutants(source_files, subjects)
80
+ mutants = generate_mutants(subjects)
81
+ bootstrap_thread.value
82
+
83
+ filtered_mutants = filter_mutants(mutants)
84
+ return filtered_mutants unless targeted_run?
85
+
86
+ refresh_coverage_for_targeted_run(filtered_mutants, source_files)
87
+ end
88
+
89
+ def refresh_coverage_for_targeted_run(mutants, source_files)
90
+ return mutants unless retry_full_bootstrap?(mutants)
91
+
92
+ bootstrap_coverage(source_files)
93
+ filter_mutants(mutants)
94
+ end
95
+
96
+ def bootstrap_mutants(source_files, subjects)
97
+ scoped_tests = scoped_bootstrap_test_files(subjects)
98
+ Thread.new { bootstrap_coverage(source_files, scoped_tests) }
99
+ end
100
+
72
101
  def execute_mutants(mutants)
73
102
  execution_engine.run(
74
103
  mutants,
@@ -94,13 +123,33 @@ module Henitai
94
123
  @result = Result.new(
95
124
  mutants:,
96
125
  started_at:,
97
- finished_at:
126
+ finished_at:,
127
+ thresholds: result_thresholds
98
128
  )
99
129
  persist_history(@result, finished_at)
100
130
  report(@result)
101
131
  @result
102
132
  end
103
133
 
134
+ # Returns the spec files to use for the coverage bootstrap.
135
+ #
136
+ # For full runs (no subject pattern given), returns nil so the bootstrapper
137
+ # falls back to the integration's full test-file list.
138
+ #
139
+ # For targeted runs, returns the union of test files selected for each
140
+ # resolved subject. Falls back to nil (all tests) if the selection is empty,
141
+ # so the bootstrapper always has a non-empty file list.
142
+ def scoped_bootstrap_test_files(subjects)
143
+ return nil if pattern_subjects.empty?
144
+
145
+ files = subjects.flat_map { |subject| integration.select_tests(subject) }.uniq
146
+ files.empty? ? nil : files
147
+ end
148
+
149
+ def bootstrap_coverage(source_files, test_files = nil)
150
+ coverage_bootstrapper.ensure!(source_files:, config:, integration:, test_files:)
151
+ end
152
+
104
153
  def subject_resolver
105
154
  @subject_resolver ||= SubjectResolver.new
106
155
  end
@@ -125,10 +174,6 @@ module Henitai
125
174
  @coverage_bootstrapper ||= CoverageBootstrapper.new
126
175
  end
127
176
 
128
- def bootstrap_coverage(source_files)
129
- coverage_bootstrapper.ensure!(source_files:, config:, integration:)
130
- end
131
-
132
177
  def integration
133
178
  @integration ||= Integration.for(config.integration).new
134
179
  end
@@ -172,6 +217,19 @@ module Henitai
172
217
  Array(@subjects)
173
218
  end
174
219
 
220
+ def targeted_run?
221
+ !pattern_subjects.empty?
222
+ end
223
+
224
+ def retry_full_bootstrap?(mutants)
225
+ executable_mutants = Array(mutants).reject do |mutant|
226
+ %i[ignored compile_error equivalent].include?(mutant.status)
227
+ end
228
+ return false if executable_mutants.empty?
229
+
230
+ executable_mutants.all? { |mutant| mutant.status == :no_coverage }
231
+ end
232
+
175
233
  def unique_subjects(subjects)
176
234
  subjects.uniq { |subject| [subject.expression, subject.source_file] }
177
235
  end
@@ -179,5 +237,11 @@ module Henitai
179
237
  def normalize_path(path)
180
238
  File.expand_path(path)
181
239
  end
240
+
241
+ def result_thresholds
242
+ return nil unless config.respond_to?(:thresholds)
243
+
244
+ config.thresholds
245
+ end
182
246
  end
183
247
  end
@@ -12,12 +12,23 @@ module Henitai
12
12
  class SourceParser
13
13
  DEFAULT_PATH = "(string)"
14
14
 
15
+ @cache = {}
16
+
15
17
  def self.parse(source, path: DEFAULT_PATH)
16
18
  new.parse(source, path:)
17
19
  end
18
20
 
21
+ # Returns the parsed AST for +path+, re-using a cached result when the
22
+ # file's mtime has not changed. This avoids parsing the same file twice
23
+ # across pipeline phases (e.g. SubjectResolver then MutantGenerator).
19
24
  def self.parse_file(path)
20
- new.parse_file(path)
25
+ key = [path, File.mtime(path)]
26
+ @cache[key] ||= new.parse_file(path)
27
+ end
28
+
29
+ # Clears the parse cache. Intended for test isolation.
30
+ def self.clear_cache!
31
+ @cache.clear
21
32
  end
22
33
 
23
34
  def parse(source, path: DEFAULT_PATH)
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require_relative "coverage_report_reader"
4
4
 
5
5
  module Henitai
6
6
  # Applies static, pre-execution filtering to generated mutants.
7
7
  class StaticFilter
8
- DEFAULT_COVERAGE_REPORT_PATH = "coverage/.resultset.json"
9
- DEFAULT_PER_TEST_COVERAGE_REPORT_PATH = "coverage/henitai_per_test.json"
8
+ DEFAULT_COVERAGE_REPORT_PATH = CoverageReportReader::DEFAULT_COVERAGE_REPORT_PATH
9
+ DEFAULT_PER_TEST_COVERAGE_REPORT_PATH = CoverageReportReader::DEFAULT_PER_TEST_COVERAGE_REPORT_PATH
10
+
11
+ def initialize(coverage_report_reader: CoverageReportReader.new)
12
+ @coverage_report_reader = coverage_report_reader
13
+ end
10
14
 
11
15
  # This method is the gate-level filter orchestrator.
12
16
  def apply(mutants, config)
@@ -32,6 +36,7 @@ module Henitai
32
36
  per_test_coverage_report_path = per_test_coverage_report_path(config)
33
37
 
34
38
  coverage_lines = coverage_lines_by_file(coverage_report_path)
39
+ coverage_lines = merge_method_coverage(coverage_lines, coverage_report_path)
35
40
  return coverage_lines unless coverage_lines.empty?
36
41
 
37
42
  coverage_lines_from_test_lines(
@@ -40,31 +45,17 @@ module Henitai
40
45
  end
41
46
 
42
47
  def coverage_lines_by_file(path = DEFAULT_COVERAGE_REPORT_PATH)
43
- return {} unless File.exist?(path)
44
-
45
- coverage = Hash.new { |hash, key| hash[key] = [] }
46
- JSON.parse(File.read(path)).each_value do |result|
47
- result.fetch("coverage", {}).each do |file, file_coverage|
48
- coverage[normalize_path(file)].concat(covered_lines(file_coverage))
49
- end
50
- end
51
-
52
- coverage.transform_values(&:uniq).transform_values(&:sort)
48
+ coverage_report_reader.coverage_lines_by_file(path)
53
49
  end
54
50
 
55
51
  def test_lines_by_file(path = DEFAULT_PER_TEST_COVERAGE_REPORT_PATH)
56
- return {} unless File.exist?(path)
57
-
58
- parsed = JSON.parse(File.read(path))
59
- return {} unless parsed.is_a?(Hash)
60
-
61
- parsed.transform_values do |coverage|
62
- normalize_test_coverage(coverage)
63
- end
52
+ coverage_report_reader.test_lines_by_file(path)
64
53
  end
65
54
 
66
55
  private
67
56
 
57
+ attr_reader :coverage_report_reader
58
+
68
59
  def ignored?(mutant, config)
69
60
  source = source_for(mutant)
70
61
  return false unless source
@@ -97,9 +88,10 @@ module Henitai
97
88
 
98
89
  def covered?(mutant, coverage_lines)
99
90
  file = normalize_path(mutant.location[:file])
100
- start_line = mutant.location[:start_line]
101
-
102
- Array(coverage_lines[file]).include?(start_line)
91
+ covered = Array(coverage_lines[file])
92
+ (mutant.location[:start_line]..mutant.location[:end_line]).any? do |line|
93
+ covered.include?(line)
94
+ end
103
95
  end
104
96
 
105
97
  def source_for(mutant)
@@ -115,23 +107,40 @@ module Henitai
115
107
  @compiled_ignore_patterns[patterns] ||= patterns.map { |pattern| Regexp.new(pattern) }
116
108
  end
117
109
 
118
- def covered_lines(file_coverage)
119
- Array(file_coverage["lines"]).each_with_index.filter_map do |count, index|
120
- index + 1 if count.to_i.positive?
110
+ def merge_method_coverage(coverage_lines, path)
111
+ return coverage_lines unless File.exist?(path)
112
+
113
+ JSON.parse(File.read(path)).each_value do |suite|
114
+ suite.fetch("coverage", {}).each do |file, file_coverage|
115
+ merge_file_method_coverage(coverage_lines, file, file_coverage)
116
+ end
121
117
  end
118
+
119
+ coverage_lines.transform_values(&:sort)
122
120
  end
123
121
 
124
- def normalize_test_coverage(coverage)
125
- case coverage
126
- when Hash
127
- coverage.transform_values do |lines|
128
- Array(lines).grep(Integer).uniq.sort
129
- end
130
- else
131
- Array(coverage).grep(Integer).uniq.sort
122
+ def merge_file_method_coverage(coverage_lines, file, file_coverage)
123
+ methods = file_coverage["methods"]
124
+ return unless methods.is_a?(Hash)
125
+
126
+ normalized = normalize_path(file)
127
+ methods.each do |key, count|
128
+ next unless count.to_i.positive?
129
+
130
+ range = method_line_range(key)
131
+ next unless range
132
+
133
+ coverage_lines[normalized] = Array(coverage_lines[normalized]) | range.to_a
132
134
  end
133
135
  end
134
136
 
137
+ def method_line_range(key)
138
+ m = key.match(/(\d+), \d+, (\d+), \d+\]\z/)
139
+ return unless m
140
+
141
+ (m.captures.first.to_i..m.captures.last.to_i)
142
+ end
143
+
135
144
  def coverage_lines_from_test_lines(test_lines)
136
145
  coverage = Hash.new { |hash, key| hash[key] = [] }
137
146
 
@@ -147,10 +156,16 @@ module Henitai
147
156
  end
148
157
 
149
158
  def normalize_path(path)
159
+ @normalize_path_cache ||= {}
160
+ return @normalize_path_cache[path] if @normalize_path_cache.key?(path)
161
+
150
162
  expanded = File.expand_path(path)
151
- File.realpath(expanded)
152
- rescue Errno::ENOENT, Errno::ENOTDIR
153
- expanded
163
+ resolved = begin
164
+ File.realpath(expanded)
165
+ rescue Errno::ENOENT, Errno::ENOTDIR
166
+ expanded
167
+ end
168
+ @normalize_path_cache[path] = resolved
154
169
  end
155
170
 
156
171
  def equivalence_detector
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Henitai
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/henitai.rb CHANGED
@@ -22,6 +22,8 @@ module Henitai
22
22
 
23
23
  autoload :Configuration, "henitai/configuration"
24
24
  autoload :CoverageBootstrapper, "henitai/coverage_bootstrapper"
25
+ autoload :CoverageReportReader, "henitai/coverage_report_reader"
26
+ autoload :PerTestCoverageSelector, "henitai/per_test_coverage_selector"
25
27
  autoload :Subject, "henitai/subject"
26
28
  autoload :Mutant, "henitai/mutant"
27
29
  autoload :Operator, "henitai/operator"
@@ -33,6 +35,7 @@ module Henitai
33
35
  autoload :MutantGenerator, "henitai/mutant_generator"
34
36
  autoload :MutantHistoryStore, "henitai/mutant_history_store"
35
37
  autoload :AridNodeFilter, "henitai/arid_node_filter"
38
+ autoload :AvailableCpuCount, "henitai/available_cpu_count"
36
39
  autoload :EquivalenceDetector, "henitai/equivalence_detector"
37
40
  autoload :StaticFilter, "henitai/static_filter"
38
41
  autoload :StillbornFilter, "henitai/stillborn_filter"