henitai 0.3.1 → 0.5.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +104 -1
- data/README.md +11 -1
- data/assets/schema/henitai.schema.json +1 -1
- data/lib/henitai/cli/operator_command.rb +2 -1
- data/lib/henitai/cli/run_options.rb +1 -1
- data/lib/henitai/cli.rb +1 -1
- data/lib/henitai/configuration.rb +9 -2
- data/lib/henitai/configuration_validator.rb +1 -1
- data/lib/henitai/dirty_source_detector.rb +53 -0
- data/lib/henitai/equivalence_detector/operand_predicates.rb +49 -0
- data/lib/henitai/equivalence_detector.rb +6 -23
- data/lib/henitai/excluded_test_filter.rb +47 -0
- data/lib/henitai/execution_engine.rb +5 -11
- data/lib/henitai/inherited_fd_registry.rb +66 -0
- data/lib/henitai/integration/base.rb +7 -2
- data/lib/henitai/integration/child_bootstrap.rb +27 -0
- data/lib/henitai/integration/child_debug_log.rb +135 -0
- data/lib/henitai/integration/child_runtime_control.rb +6 -18
- data/lib/henitai/integration/loaded_features.rb +38 -0
- data/lib/henitai/integration/mutant_run_support.rb +5 -5
- data/lib/henitai/integration/rspec_child_runner.rb +16 -15
- data/lib/henitai/integration/rspec_process_runner.rb +7 -2
- data/lib/henitai/integration.rb +10 -7
- data/lib/henitai/mutation_skip_directives.rb +7 -1
- data/lib/henitai/operator.rb +12 -2
- data/lib/henitai/operators/hash_key_type.rb +50 -0
- data/lib/henitai/operators/hash_literal.rb +19 -20
- data/lib/henitai/operators/return_value.rb +1 -1
- data/lib/henitai/operators.rb +1 -0
- data/lib/henitai/orphan_watchdog.rb +93 -0
- data/lib/henitai/process_liveness.rb +41 -0
- data/lib/henitai/reports_directory_lock.rb +12 -11
- data/lib/henitai/result.rb +30 -3
- data/lib/henitai/runner.rb +41 -123
- data/lib/henitai/runner_dependencies.rb +75 -0
- data/lib/henitai/slot_scheduler/drain_verdict.rb +29 -0
- data/lib/henitai/slot_scheduler/draining.rb +7 -17
- data/lib/henitai/slot_scheduler/retry_policy.rb +21 -0
- data/lib/henitai/slot_scheduler/slot_deadline.rb +37 -0
- data/lib/henitai/slot_scheduler/slot_table.rb +75 -0
- data/lib/henitai/slot_scheduler/test_file_selection.rb +40 -0
- data/lib/henitai/slot_scheduler.rb +68 -80
- data/lib/henitai/source_file_selection.rb +76 -0
- data/lib/henitai/subject_selection.rb +33 -0
- data/lib/henitai/survivor_rerun_strategy.rb +7 -19
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +8 -0
- data/sig/henitai.rbs +94 -38
- metadata +32 -9
- data/lib/henitai/integration/child_debug_support.rb +0 -119
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
class SlotScheduler
|
|
5
|
+
# Resolves the test files a mutant should be run against.
|
|
6
|
+
#
|
|
7
|
+
# Three sources, in precedence order: a caller-supplied resolver lambda, a
|
|
8
|
+
# fixed list, or the integration's own per-subject selection. The first two
|
|
9
|
+
# exist so a survivor rerun or a per-test-coverage run can narrow the
|
|
10
|
+
# selection without the integration knowing about it.
|
|
11
|
+
#
|
|
12
|
+
# Precedence is by key *presence*, not truthiness: an explicit empty list
|
|
13
|
+
# means "no tests cover this", which the scheduler turns into
|
|
14
|
+
# `:no_coverage`. Falling through to the integration there would silently
|
|
15
|
+
# widen the selection back out.
|
|
16
|
+
class TestFileSelection
|
|
17
|
+
def initialize(options:, integration:)
|
|
18
|
+
@options = options
|
|
19
|
+
@integration = integration
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def for(mutant)
|
|
23
|
+
if @options.key?(:test_file_resolver)
|
|
24
|
+
@options[:test_file_resolver].call(mutant)
|
|
25
|
+
elsif @options.key?(:test_files)
|
|
26
|
+
@options[:test_files]
|
|
27
|
+
else
|
|
28
|
+
@integration.select_tests(mutant.subject)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# True when the resolver was asked and came back empty. Only meaningful
|
|
33
|
+
# for a resolver-driven run: a fixed empty `test_files` list is a
|
|
34
|
+
# deliberate "run nothing", not an absence of coverage.
|
|
35
|
+
def resolved_empty?(test_files)
|
|
36
|
+
@options.key?(:test_file_resolver) && test_files.empty?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "slot_scheduler/process_control"
|
|
4
4
|
require_relative "slot_scheduler/draining"
|
|
5
|
+
require_relative "slot_scheduler/drain_verdict"
|
|
6
|
+
require_relative "slot_scheduler/retry_policy"
|
|
7
|
+
require_relative "slot_scheduler/slot_deadline"
|
|
8
|
+
require_relative "slot_scheduler/slot_table"
|
|
9
|
+
require_relative "slot_scheduler/test_file_selection"
|
|
5
10
|
|
|
6
11
|
module Henitai
|
|
7
12
|
# Owns the process-slot table for a single parallel mutation run.
|
|
@@ -40,18 +45,23 @@ module Henitai
|
|
|
40
45
|
# @return [Array<ScenarioExecutionResult>] verdicts accumulated so far.
|
|
41
46
|
attr_reader :flaky_retry_count, :results
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
# +slot_table+ is injectable so a spec can seed mid-run state, or assert
|
|
49
|
+
# against the table through its own public interface, without reaching a
|
|
50
|
+
# private reader here.
|
|
51
|
+
# rubocop:disable Metrics/ParameterLists -- every one of these is a distinct
|
|
52
|
+
# collaborator supplied by ProcessWorkerRunner; bundling them into a context
|
|
53
|
+
# object would only move the list.
|
|
54
|
+
def initialize(integration:, config:, progress_reporter:, options:, host:, slot_table: SlotTable.new)
|
|
55
|
+
# rubocop:enable Metrics/ParameterLists
|
|
44
56
|
@integration = integration
|
|
45
57
|
@config = config
|
|
46
58
|
@progress_reporter = progress_reporter
|
|
47
59
|
@options = options
|
|
48
60
|
@host = host
|
|
61
|
+
@slot_table = slot_table
|
|
49
62
|
@pending = []
|
|
50
|
-
@slots = {}
|
|
51
|
-
@pid_to_slot = {}
|
|
52
63
|
@results = []
|
|
53
64
|
@flaky_retry_count = 0
|
|
54
|
-
@next_slot_id = 0
|
|
55
65
|
end
|
|
56
66
|
|
|
57
67
|
# Queues the mutants to be scheduled into worker slots.
|
|
@@ -60,11 +70,11 @@ module Henitai
|
|
|
60
70
|
end
|
|
61
71
|
|
|
62
72
|
def done?
|
|
63
|
-
pending.empty? &&
|
|
73
|
+
pending.empty? && slot_table.empty?
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
def fill_idle_slots
|
|
67
|
-
while
|
|
77
|
+
while slot_table.size < worker_count && !pending.empty?
|
|
68
78
|
mutant = pending.shift
|
|
69
79
|
spawn_into_slot(mutant)
|
|
70
80
|
end
|
|
@@ -83,8 +93,8 @@ module Henitai
|
|
|
83
93
|
|
|
84
94
|
def next_event_timeout
|
|
85
95
|
now = monotonic_time
|
|
86
|
-
slot_timeouts =
|
|
87
|
-
|
|
96
|
+
slot_timeouts = slot_table.each_value.filter_map do |slot|
|
|
97
|
+
slot_deadline.remaining(slot, now)
|
|
88
98
|
end
|
|
89
99
|
|
|
90
100
|
slot_timeouts.min
|
|
@@ -92,7 +102,7 @@ module Henitai
|
|
|
92
102
|
|
|
93
103
|
private
|
|
94
104
|
|
|
95
|
-
attr_reader :pending, :
|
|
105
|
+
attr_reader :pending, :slot_table, :integration, :config,
|
|
96
106
|
:progress_reporter, :options, :host
|
|
97
107
|
|
|
98
108
|
def worker_count = host.worker_count
|
|
@@ -101,12 +111,12 @@ module Henitai
|
|
|
101
111
|
def shutdown? = host.shutdown_requested?
|
|
102
112
|
|
|
103
113
|
def spawn_into_slot(mutant)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
mutant
|
|
107
|
-
return record_no_coverage(mutant) if
|
|
114
|
+
selection = test_file_selection
|
|
115
|
+
test_files = selection.for(mutant)
|
|
116
|
+
annotate_selection(mutant, test_files)
|
|
117
|
+
return record_no_coverage(mutant) if selection.resolved_empty?(test_files)
|
|
108
118
|
|
|
109
|
-
worker_index = next_free_worker_index
|
|
119
|
+
worker_index = slot_table.next_free_worker_index(worker_count)
|
|
110
120
|
with_worker_slot(worker_index) do
|
|
111
121
|
handle = integration.spawn_mutant(mutant: mutant, test_files: test_files)
|
|
112
122
|
register_slot(handle, mutant, worker_index, slot_timeout(mutant, test_files))
|
|
@@ -115,11 +125,18 @@ module Henitai
|
|
|
115
125
|
record_spawn_failure(mutant, e)
|
|
116
126
|
end
|
|
117
127
|
|
|
128
|
+
# Reported back onto the mutant so the report can show which tests were
|
|
129
|
+
# considered, whether or not any of them ran. Guarded by respond_to? because
|
|
130
|
+
# specs pass Struct stand-ins without these members.
|
|
131
|
+
def annotate_selection(mutant, test_files)
|
|
132
|
+
mutant.covered_by = test_files if mutant.respond_to?(:covered_by=)
|
|
133
|
+
mutant.tests_completed = test_files.size if mutant.respond_to?(:tests_completed=)
|
|
134
|
+
end
|
|
135
|
+
|
|
118
136
|
def register_slot(handle, mutant, worker_index, timeout)
|
|
119
|
-
|
|
120
|
-
slot
|
|
121
|
-
|
|
122
|
-
pid_to_slot[handle.pid] = slot_id
|
|
137
|
+
slot = build_slot(slot_table.next_slot_id!, mutant, handle, worker_index, timeout)
|
|
138
|
+
slot_table.add(slot)
|
|
139
|
+
slot_table.register_pid(handle.pid, slot.slot_id)
|
|
123
140
|
Integration::SchedulerDiagnostics.child_started(handle.pid)
|
|
124
141
|
end
|
|
125
142
|
|
|
@@ -137,20 +154,11 @@ module Henitai
|
|
|
137
154
|
resolver ? resolver.call(mutant, test_files) : config.timeout
|
|
138
155
|
end
|
|
139
156
|
|
|
140
|
-
# Smallest index in 0...worker_count not held by a live slot, so
|
|
141
|
-
# concurrently-running children always see distinct values and freed
|
|
142
|
-
# indices are reused. Slot ids themselves grow monotonically and are
|
|
143
|
-
# unsuitable as a resource token.
|
|
144
|
-
def next_free_worker_index
|
|
145
|
-
used = slots.each_value.map(&:worker_index)
|
|
146
|
-
(0...worker_count).find { |index| !used.include?(index) } || used.size
|
|
147
|
-
end
|
|
148
|
-
|
|
149
157
|
def complete_slot(pid, wait_result)
|
|
150
|
-
slot_id =
|
|
158
|
+
slot_id = slot_table.release_pid(pid)
|
|
151
159
|
return unless slot_id
|
|
152
160
|
|
|
153
|
-
slot =
|
|
161
|
+
slot = slot_table.fetch(slot_id)
|
|
154
162
|
return unless slot
|
|
155
163
|
|
|
156
164
|
Integration::SchedulerDiagnostics.child_ended(pid)
|
|
@@ -159,29 +167,46 @@ module Henitai
|
|
|
159
167
|
end
|
|
160
168
|
|
|
161
169
|
def dispatch_slot_result(slot, result)
|
|
162
|
-
if
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
return retry_slot(slot) if retry_policy.retry?(slot: slot, result: result, shutdown: shutdown?)
|
|
171
|
+
|
|
172
|
+
finalize_slot(slot, result)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def finalize_slot(slot, result)
|
|
176
|
+
slot_table.delete(slot.slot_id)
|
|
177
|
+
slot.mutant.status = result.status
|
|
178
|
+
results << result
|
|
179
|
+
progress_reporter&.progress(slot.mutant, scenario_result: result)
|
|
180
|
+
result.release_output! if result.respond_to?(:release_output!)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Memoized lazily, not built in #initialize: specs (and the no-op scheduler
|
|
184
|
+
# the runner builds when nothing is pending) pass `config: nil`, and this
|
|
185
|
+
# is only reached once a slot has actually finished.
|
|
186
|
+
def retry_policy
|
|
187
|
+
@retry_policy ||= RetryPolicy.new(max_retries: config.max_flaky_retries)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def drain_verdict
|
|
191
|
+
@drain_verdict ||= DrainVerdict.new(integration: integration)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def slot_deadline
|
|
195
|
+
@slot_deadline ||= SlotDeadline.new(drain_window: PROCESS_DRAIN_WINDOW)
|
|
171
196
|
end
|
|
172
197
|
|
|
173
|
-
def
|
|
174
|
-
|
|
198
|
+
def test_file_selection
|
|
199
|
+
@test_file_selection ||= TestFileSelection.new(options: options, integration: integration)
|
|
175
200
|
end
|
|
176
201
|
|
|
177
202
|
def retry_slot(slot)
|
|
178
|
-
test_files =
|
|
203
|
+
test_files = test_file_selection.for(slot.mutant)
|
|
179
204
|
with_worker_slot(slot.worker_index) do
|
|
180
205
|
handle = integration.spawn_mutant(mutant: slot.mutant, test_files: test_files)
|
|
181
206
|
finish_retry(slot, handle)
|
|
182
207
|
end
|
|
183
208
|
rescue StandardError => e
|
|
184
|
-
|
|
209
|
+
slot_table.delete(slot.slot_id)
|
|
185
210
|
record_spawn_failure(slot.mutant, e)
|
|
186
211
|
end
|
|
187
212
|
|
|
@@ -189,7 +214,7 @@ module Henitai
|
|
|
189
214
|
@flaky_retry_count += 1 if slot.retry_count.zero?
|
|
190
215
|
slot.retry_count += 1
|
|
191
216
|
reset_slot_for_retry(slot, handle)
|
|
192
|
-
|
|
217
|
+
slot_table.register_pid(handle.pid, slot.slot_id)
|
|
193
218
|
Integration::SchedulerDiagnostics.child_started(handle.pid)
|
|
194
219
|
end
|
|
195
220
|
|
|
@@ -220,43 +245,6 @@ module Henitai
|
|
|
220
245
|
progress_reporter&.progress(mutant, scenario_result: nil)
|
|
221
246
|
end
|
|
222
247
|
|
|
223
|
-
def resolved_selection_empty?(test_files)
|
|
224
|
-
options.key?(:test_file_resolver) && test_files.empty?
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def remaining_slot_timeout(slot, now)
|
|
228
|
-
# Invariant: drain_draining_slots runs (and removes draining slots) before
|
|
229
|
-
# the event wait, so next_event_timeout never observes a draining slot
|
|
230
|
-
# whose SIGTERM has not been sent. Guard term_sent_at_monotonic defensively
|
|
231
|
-
# against a future ordering change: an unsignalled draining slot is due now.
|
|
232
|
-
return 0.0 if slot.draining && slot.term_sent_at_monotonic.nil?
|
|
233
|
-
|
|
234
|
-
deadline =
|
|
235
|
-
if slot.draining
|
|
236
|
-
slot.term_sent_at_monotonic + PROCESS_DRAIN_WINDOW
|
|
237
|
-
else
|
|
238
|
-
slot.started_at_monotonic + slot.timeout
|
|
239
|
-
end
|
|
240
|
-
remaining = deadline - now
|
|
241
|
-
remaining.positive? ? remaining : 0.0
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def resolve_test_files(mutant)
|
|
245
|
-
if @options.key?(:test_file_resolver)
|
|
246
|
-
@options[:test_file_resolver].call(mutant)
|
|
247
|
-
elsif @options.key?(:test_files)
|
|
248
|
-
@options[:test_files]
|
|
249
|
-
else
|
|
250
|
-
integration.select_tests(mutant.subject)
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def next_slot_id!
|
|
255
|
-
id = @next_slot_id
|
|
256
|
-
@next_slot_id += 1
|
|
257
|
-
id
|
|
258
|
-
end
|
|
259
|
-
|
|
260
248
|
def with_worker_slot(worker_index)
|
|
261
249
|
previous = ENV.fetch(WORKER_SLOT_ENV, nil)
|
|
262
250
|
ENV[WORKER_SLOT_ENV] = worker_index.to_s
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
# Resolves which source files a run mutates: everything under `includes:`,
|
|
5
|
+
# minus anything matched by `excludes:`, optionally narrowed to what changed
|
|
6
|
+
# since a git ref.
|
|
7
|
+
#
|
|
8
|
+
# Excludes are absolute, not a tiebreak: an excluded file (a standalone entry
|
|
9
|
+
# point that cannot be mutation-tested in-process, say) stays excluded even
|
|
10
|
+
# when it is the only thing that changed. Both stages are filters over the
|
|
11
|
+
# same list, so they commute — excludes run first only to keep the
|
|
12
|
+
# per-test-coverage lookups in `filter_changed` off files that are already
|
|
13
|
+
# out of scope.
|
|
14
|
+
class SourceFileSelection
|
|
15
|
+
def initialize(config:, since:, git_diff_analyzer:, per_test_coverage:)
|
|
16
|
+
@config = config
|
|
17
|
+
@since = since
|
|
18
|
+
@git_diff_analyzer = git_diff_analyzer
|
|
19
|
+
@per_test_coverage = per_test_coverage
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
filter_changed(reject_excluded(included_source_files))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def included_source_files
|
|
27
|
+
Array(@config.includes).flat_map do |include_path|
|
|
28
|
+
Dir.glob(File.join(include_path, "**", "*.rb"))
|
|
29
|
+
end.uniq
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Drops files matched by any `excludes:` glob. Compared as expanded paths so
|
|
33
|
+
# a relative glob and an absolute candidate still match.
|
|
34
|
+
def reject_excluded(files)
|
|
35
|
+
excluded = excluded_source_files
|
|
36
|
+
return files if excluded.empty?
|
|
37
|
+
|
|
38
|
+
files.reject { |path| excluded.include?(normalize_path(path)) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def filter_changed(files)
|
|
42
|
+
return files unless @since
|
|
43
|
+
|
|
44
|
+
changed = changed_paths_since.map { |path| normalize_path(path) }
|
|
45
|
+
changed += covered_sources_for_changed_tests(changed)
|
|
46
|
+
files.select { |path| changed.include?(normalize_path(path)) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def excluded_source_files
|
|
52
|
+
Array(@config.excludes)
|
|
53
|
+
.flat_map { |pattern| Dir.glob(pattern) }
|
|
54
|
+
.map { |path| normalize_path(path) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Committed changes since the ref plus the current working tree (tracked
|
|
58
|
+
# dirty and untracked files): the working tree is what gets tested, so it is
|
|
59
|
+
# always part of "changed since REF".
|
|
60
|
+
def changed_paths_since
|
|
61
|
+
@git_diff_analyzer.changed_files(from: @since, to: "HEAD") +
|
|
62
|
+
@git_diff_analyzer.working_tree_changed_files
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Changed test files select the source files they cover, so an edited test
|
|
66
|
+
# re-tests the subjects it can kill. Uses the per-test map from the previous
|
|
67
|
+
# run — Gate 1 runs before this run's bootstrap finishes.
|
|
68
|
+
def covered_sources_for_changed_tests(changed_paths)
|
|
69
|
+
changed_paths
|
|
70
|
+
.flat_map { |path| @per_test_coverage.source_files_covered_by(path) }
|
|
71
|
+
.map { |path| normalize_path(path) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def normalize_path(path) = File.expand_path(path)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
# Turns a list of source files into the subjects a run mutates, applying any
|
|
5
|
+
# CLI subject patterns.
|
|
6
|
+
#
|
|
7
|
+
# With no patterns every resolved subject is kept. With patterns, each is
|
|
8
|
+
# applied independently and the results concatenated, so overlapping patterns
|
|
9
|
+
# can name the same subject twice — hence the de-duplication on
|
|
10
|
+
# `[expression, source_file]` rather than on object identity. Expression alone
|
|
11
|
+
# is not enough: the same expression can legitimately appear in two files.
|
|
12
|
+
class SubjectSelection
|
|
13
|
+
def initialize(subject_resolver:, patterns:)
|
|
14
|
+
@subject_resolver = subject_resolver
|
|
15
|
+
@patterns = patterns
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def resolve(source_files)
|
|
19
|
+
subjects = @subject_resolver.resolve_from_files(source_files)
|
|
20
|
+
return subjects if pattern_expressions.empty?
|
|
21
|
+
|
|
22
|
+
unique(pattern_expressions.flat_map { |expression| @subject_resolver.apply_pattern(subjects, expression) })
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def unique(subjects)
|
|
26
|
+
subjects.uniq { |subject| [subject.expression, subject.source_file] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def pattern_expressions = Array(@patterns).map(&:expression)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "dirty_source_detector"
|
|
4
|
+
|
|
3
5
|
module Henitai
|
|
4
6
|
# Survivor-rerun fast path for {Runner}.
|
|
5
7
|
#
|
|
@@ -154,27 +156,13 @@ module Henitai
|
|
|
154
156
|
end
|
|
155
157
|
|
|
156
158
|
def dirty_source_files?(dirty_worktree_files, git_sha: nil)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
all_changed = dirty_worktree_files + committed_changed_files(git_sha)
|
|
160
|
-
include_roots = Array(@config.includes).map { |path| normalize_path(path) }
|
|
161
|
-
all_changed.any? { |path| in_include_root?(normalize_path(path), include_roots) }
|
|
162
|
-
rescue StandardError
|
|
163
|
-
true
|
|
159
|
+
dirty_source_detector.dirty?(dirty_worktree_files, git_sha: git_sha)
|
|
164
160
|
end
|
|
165
161
|
|
|
166
|
-
def
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def in_include_root?(path, include_roots)
|
|
173
|
-
include_roots.any? { |root| path == root || path.start_with?("#{root}/") }
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def normalize_path(path)
|
|
177
|
-
File.expand_path(path)
|
|
162
|
+
def dirty_source_detector
|
|
163
|
+
@dirty_source_detector ||= DirtySourceDetector.new(
|
|
164
|
+
includes: @config.includes, git_diff_analyzer: @git_diff_analyzer
|
|
165
|
+
)
|
|
178
166
|
end
|
|
179
167
|
|
|
180
168
|
def warn_survivor_drift(selector)
|
data/lib/henitai/version.rb
CHANGED
data/lib/henitai.rb
CHANGED
|
@@ -58,6 +58,9 @@ module Henitai
|
|
|
58
58
|
autoload :SurvivorTestFilter, "henitai/survivor_test_filter"
|
|
59
59
|
autoload :SurvivorActivationCache, "henitai/survivor_activation_cache"
|
|
60
60
|
autoload :SurvivorRerunStrategy, "henitai/survivor_rerun_strategy"
|
|
61
|
+
autoload :DirtySourceDetector, "henitai/dirty_source_detector"
|
|
62
|
+
autoload :SourceFileSelection, "henitai/source_file_selection"
|
|
63
|
+
autoload :SubjectSelection, "henitai/subject_selection"
|
|
61
64
|
autoload :ScenarioExecutionResult, "henitai/scenario_execution_result"
|
|
62
65
|
autoload :CoverageFormatter, "henitai/coverage_formatter"
|
|
63
66
|
autoload :MinitestCoverageReporter, "henitai/minitest_coverage_reporter"
|
|
@@ -65,12 +68,17 @@ module Henitai
|
|
|
65
68
|
autoload :SyntaxValidator, "henitai/syntax_validator"
|
|
66
69
|
autoload :SamplingStrategy, "henitai/sampling_strategy"
|
|
67
70
|
autoload :TestPrioritizer, "henitai/test_prioritizer"
|
|
71
|
+
autoload :ExcludedTestFilter, "henitai/excluded_test_filter"
|
|
68
72
|
autoload :TimeoutCalibrator, "henitai/timeout_calibrator"
|
|
69
73
|
autoload :ExecutionEngine, "henitai/execution_engine"
|
|
70
74
|
autoload :ProcessWorkerRunner, "henitai/process_worker_runner"
|
|
71
75
|
autoload :SlotScheduler, "henitai/slot_scheduler"
|
|
72
76
|
autoload :ProcessWakeup, "henitai/process_wakeup"
|
|
77
|
+
autoload :ProcessLiveness, "henitai/process_liveness"
|
|
78
|
+
autoload :InheritedFdRegistry, "henitai/inherited_fd_registry"
|
|
79
|
+
autoload :OrphanWatchdog, "henitai/orphan_watchdog"
|
|
73
80
|
autoload :Runner, "henitai/runner"
|
|
81
|
+
autoload :RunnerDependencies, "henitai/runner_dependencies"
|
|
74
82
|
autoload :Reporter, "henitai/reporter"
|
|
75
83
|
autoload :Integration, "henitai/integration"
|
|
76
84
|
autoload :Result, "henitai/result"
|