henitai 0.1.2 → 0.1.4
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 +105 -1
- data/README.md +3 -1
- data/assets/schema/henitai.schema.json +0 -4
- data/lib/henitai/arid_node_filter.rb +3 -0
- data/lib/henitai/available_cpu_count.rb +79 -0
- data/lib/henitai/cli.rb +7 -4
- data/lib/henitai/configuration.rb +3 -5
- data/lib/henitai/configuration_validator.rb +1 -11
- data/lib/henitai/coverage_bootstrapper.rb +128 -10
- data/lib/henitai/coverage_report_reader.rb +67 -0
- data/lib/henitai/eager_load.rb +11 -0
- data/lib/henitai/equivalence_detector.rb +60 -1
- data/lib/henitai/execution_engine.rb +34 -22
- data/lib/henitai/integration/rspec_process_runner.rb +58 -0
- data/lib/henitai/integration.rb +195 -110
- data/lib/henitai/mutant.rb +3 -1
- data/lib/henitai/mutant_generator.rb +25 -48
- data/lib/henitai/operator.rb +6 -1
- data/lib/henitai/operators/assignment_expression.rb +7 -23
- data/lib/henitai/operators/conditional_expression.rb +1 -7
- data/lib/henitai/operators/method_chain_unwrap.rb +41 -0
- data/lib/henitai/operators/regex_mutator.rb +89 -0
- data/lib/henitai/operators/string_literal.rb +2 -1
- data/lib/henitai/operators/unary_operator.rb +36 -0
- data/lib/henitai/operators/update_operator.rb +70 -0
- data/lib/henitai/operators.rb +4 -0
- data/lib/henitai/parallel_execution_runner.rb +135 -0
- data/lib/henitai/per_test_coverage_selector.rb +60 -0
- data/lib/henitai/reporter.rb +14 -2
- data/lib/henitai/result.rb +16 -4
- data/lib/henitai/runner.rb +75 -11
- data/lib/henitai/scenario_execution_result.rb +31 -2
- data/lib/henitai/source_parser.rb +12 -1
- data/lib/henitai/static_filter.rb +20 -41
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +3 -0
- data/sig/henitai.rbs +66 -10
- metadata +17 -4
data/lib/henitai/result.rb
CHANGED
|
@@ -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:
|
|
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)
|
data/lib/henitai/runner.rb
CHANGED
|
@@ -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 =
|
|
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,
|
|
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
|
|
@@ -5,6 +5,16 @@ module Henitai
|
|
|
5
5
|
class ScenarioExecutionResult
|
|
6
6
|
attr_reader :status, :stdout, :stderr, :exit_status, :log_path
|
|
7
7
|
|
|
8
|
+
def self.build(wait_result:, stdout:, stderr:, log_path:)
|
|
9
|
+
new(
|
|
10
|
+
status: status_for(wait_result),
|
|
11
|
+
stdout: stdout,
|
|
12
|
+
stderr: stderr,
|
|
13
|
+
log_path: log_path,
|
|
14
|
+
exit_status: exit_status_for(wait_result)
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
8
18
|
def initialize(status:, stdout:, stderr:, log_path:, exit_status: nil)
|
|
9
19
|
@status = status
|
|
10
20
|
@stdout = stdout.to_s
|
|
@@ -51,11 +61,11 @@ module Henitai
|
|
|
51
61
|
log_text.lines.last(lines).join
|
|
52
62
|
end
|
|
53
63
|
|
|
54
|
-
def should_show_logs?(all_logs:
|
|
64
|
+
def should_show_logs?(all_logs: nil)
|
|
55
65
|
all_logs || timeout?
|
|
56
66
|
end
|
|
57
67
|
|
|
58
|
-
def failure_tail(all_logs:
|
|
68
|
+
def failure_tail(all_logs: nil, lines: 12)
|
|
59
69
|
return combined_output if all_logs
|
|
60
70
|
return "" unless should_show_logs?(all_logs:)
|
|
61
71
|
|
|
@@ -64,6 +74,25 @@ module Henitai
|
|
|
64
74
|
|
|
65
75
|
private
|
|
66
76
|
|
|
77
|
+
class << self
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def status_for(wait_result)
|
|
81
|
+
return :timeout if wait_result == :timeout
|
|
82
|
+
return :compile_error if exit_status_for(wait_result) == 2
|
|
83
|
+
return :survived if wait_result.respond_to?(:success?) && wait_result.success?
|
|
84
|
+
|
|
85
|
+
:killed
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def exit_status_for(wait_result)
|
|
89
|
+
return nil if wait_result == :timeout
|
|
90
|
+
return nil unless wait_result.respond_to?(:exitstatus)
|
|
91
|
+
|
|
92
|
+
wait_result.exitstatus
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
67
96
|
def stream_section(name, content)
|
|
68
97
|
"#{name}:\n#{content}"
|
|
69
98
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
9
|
-
DEFAULT_PER_TEST_COVERAGE_REPORT_PATH =
|
|
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)
|
|
@@ -41,31 +45,17 @@ module Henitai
|
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
def coverage_lines_by_file(path = DEFAULT_COVERAGE_REPORT_PATH)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
coverage = Hash.new { |hash, key| hash[key] = [] }
|
|
47
|
-
JSON.parse(File.read(path)).each_value do |result|
|
|
48
|
-
result.fetch("coverage", {}).each do |file, file_coverage|
|
|
49
|
-
coverage[normalize_path(file)].concat(covered_lines(file_coverage))
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
coverage.transform_values(&:uniq).transform_values(&:sort)
|
|
48
|
+
coverage_report_reader.coverage_lines_by_file(path)
|
|
54
49
|
end
|
|
55
50
|
|
|
56
51
|
def test_lines_by_file(path = DEFAULT_PER_TEST_COVERAGE_REPORT_PATH)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
parsed = JSON.parse(File.read(path))
|
|
60
|
-
return {} unless parsed.is_a?(Hash)
|
|
61
|
-
|
|
62
|
-
parsed.transform_values do |coverage|
|
|
63
|
-
normalize_test_coverage(coverage)
|
|
64
|
-
end
|
|
52
|
+
coverage_report_reader.test_lines_by_file(path)
|
|
65
53
|
end
|
|
66
54
|
|
|
67
55
|
private
|
|
68
56
|
|
|
57
|
+
attr_reader :coverage_report_reader
|
|
58
|
+
|
|
69
59
|
def ignored?(mutant, config)
|
|
70
60
|
source = source_for(mutant)
|
|
71
61
|
return false unless source
|
|
@@ -151,23 +141,6 @@ module Henitai
|
|
|
151
141
|
(m.captures.first.to_i..m.captures.last.to_i)
|
|
152
142
|
end
|
|
153
143
|
|
|
154
|
-
def covered_lines(file_coverage)
|
|
155
|
-
Array(file_coverage["lines"]).each_with_index.filter_map do |count, index|
|
|
156
|
-
index + 1 if count.to_i.positive?
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def normalize_test_coverage(coverage)
|
|
161
|
-
case coverage
|
|
162
|
-
when Hash
|
|
163
|
-
coverage.transform_values do |lines|
|
|
164
|
-
Array(lines).grep(Integer).uniq.sort
|
|
165
|
-
end
|
|
166
|
-
else
|
|
167
|
-
Array(coverage).grep(Integer).uniq.sort
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
144
|
def coverage_lines_from_test_lines(test_lines)
|
|
172
145
|
coverage = Hash.new { |hash, key| hash[key] = [] }
|
|
173
146
|
|
|
@@ -183,10 +156,16 @@ module Henitai
|
|
|
183
156
|
end
|
|
184
157
|
|
|
185
158
|
def normalize_path(path)
|
|
159
|
+
@normalize_path_cache ||= {}
|
|
160
|
+
return @normalize_path_cache[path] if @normalize_path_cache.key?(path)
|
|
161
|
+
|
|
186
162
|
expanded = File.expand_path(path)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
163
|
+
resolved = begin
|
|
164
|
+
File.realpath(expanded)
|
|
165
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
166
|
+
expanded
|
|
167
|
+
end
|
|
168
|
+
@normalize_path_cache[path] = resolved
|
|
190
169
|
end
|
|
191
170
|
|
|
192
171
|
def equivalence_detector
|
data/lib/henitai/version.rb
CHANGED
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"
|
data/sig/henitai.rbs
CHANGED
|
@@ -112,6 +112,7 @@ module Henitai
|
|
|
112
112
|
attr_reader exit_status: Integer?
|
|
113
113
|
attr_reader log_path: String
|
|
114
114
|
|
|
115
|
+
def self.build: (wait_result: untyped, stdout: String?, stderr: String?, log_path: String) -> ScenarioExecutionResult
|
|
115
116
|
def initialize: (status: Symbol, stdout: String?, stderr: String?, log_path: String, ?exit_status: Integer?) -> void
|
|
116
117
|
def survived?: () -> bool
|
|
117
118
|
def killed?: () -> bool
|
|
@@ -121,6 +122,11 @@ module Henitai
|
|
|
121
122
|
def tail: (?Integer) -> String
|
|
122
123
|
def should_show_logs?: (?all_logs: bool) -> bool
|
|
123
124
|
def failure_tail: (?all_logs: bool, ?lines: Integer) -> String
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def self.status_for: (untyped) -> Symbol
|
|
129
|
+
def self.exit_status_for: (untyped) -> Integer?
|
|
124
130
|
end
|
|
125
131
|
|
|
126
132
|
class Subject
|
|
@@ -154,6 +160,8 @@ module Henitai
|
|
|
154
160
|
attr_accessor status: Symbol
|
|
155
161
|
attr_accessor killing_test: String?
|
|
156
162
|
attr_accessor duration: Float?
|
|
163
|
+
attr_accessor covered_by: Array[String]?
|
|
164
|
+
attr_accessor tests_completed: Integer?
|
|
157
165
|
|
|
158
166
|
def initialize: (subject: Subject, operator: String, nodes: Hash[Symbol, untyped], description: String, location: Hash[Symbol, untyped]) -> void
|
|
159
167
|
def killed?: () -> bool
|
|
@@ -223,15 +231,30 @@ module Henitai
|
|
|
223
231
|
def test_files: () -> Array[String]
|
|
224
232
|
def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
|
|
225
233
|
def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
|
|
234
|
+
def per_test_coverage_supported?: () -> bool
|
|
235
|
+
def wait_with_timeout: (Integer, Float) -> untyped
|
|
236
|
+
def reap_child: (Integer) -> void
|
|
237
|
+
def cleanup_process_group: (Integer) -> void
|
|
226
238
|
|
|
227
239
|
private
|
|
228
240
|
|
|
229
241
|
def pause: (Float) -> void
|
|
242
|
+
def handle_timeout: (Integer) -> Symbol
|
|
243
|
+
def cleanup_child_process: (Integer) -> void
|
|
244
|
+
def rspec_options: () -> Array[String]
|
|
245
|
+
def subprocess_env: () -> Hash[String, String]
|
|
246
|
+
def scenario_log_support: () -> ScenarioLogSupport
|
|
247
|
+
def with_subprocess_env: () { () -> untyped } -> untyped
|
|
248
|
+
def restore_subprocess_env: (Hash[String, String?]) -> void
|
|
249
|
+
def spawn_suite_process: (Array[String], Hash[Symbol, String]) -> Integer
|
|
230
250
|
end
|
|
231
251
|
|
|
232
252
|
class ScenarioLogSupport
|
|
233
253
|
def capture_child_output: (Hash[Symbol, String]) { () -> untyped } -> untyped
|
|
234
254
|
def with_coverage_dir: (String) { () -> untyped } -> untyped
|
|
255
|
+
def read_log_file: (String) -> String
|
|
256
|
+
def write_combined_log: (String, String, String) -> void
|
|
257
|
+
def combined_log: (String, String) -> String
|
|
235
258
|
|
|
236
259
|
private
|
|
237
260
|
|
|
@@ -248,6 +271,11 @@ module Henitai
|
|
|
248
271
|
def stderr_stream: () -> IO
|
|
249
272
|
end
|
|
250
273
|
|
|
274
|
+
class RspecProcessRunner
|
|
275
|
+
def run_mutant: (Rspec, mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
|
|
276
|
+
def run_suite: (Rspec, Array[String], timeout: Float) -> ScenarioExecutionResult
|
|
277
|
+
end
|
|
278
|
+
|
|
251
279
|
class Rspec < Base
|
|
252
280
|
REQUIRE_DIRECTIVE_PATTERN: Regexp
|
|
253
281
|
DEFAULT_SUITE_TIMEOUT: Float
|
|
@@ -256,13 +284,12 @@ module Henitai
|
|
|
256
284
|
def test_files: () -> Array[String]
|
|
257
285
|
def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
|
|
258
286
|
def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
|
|
287
|
+
def per_test_coverage_supported?: () -> bool
|
|
259
288
|
|
|
260
289
|
private
|
|
261
290
|
|
|
262
291
|
def run_in_child: (mutant: Mutant, test_files: Array[String], log_paths: Hash[Symbol, String]) -> Integer
|
|
263
292
|
def suite_command: (Array[String]) -> Array[String]
|
|
264
|
-
def wait_with_timeout: (Integer, Float) -> untyped
|
|
265
|
-
def handle_timeout: (Integer) -> Symbol
|
|
266
293
|
def build_result: (untyped, Hash[Symbol, String]) -> ScenarioExecutionResult
|
|
267
294
|
def scenario_status: (untyped) -> Symbol
|
|
268
295
|
def exit_status_for: (untyped) -> Integer?
|
|
@@ -271,8 +298,7 @@ module Henitai
|
|
|
271
298
|
def combined_log: (String, String) -> String
|
|
272
299
|
def scenario_log_paths: (String) -> Hash[Symbol, String]
|
|
273
300
|
def run_tests: (Array[String]) -> Integer
|
|
274
|
-
def
|
|
275
|
-
def rspec_options: () -> Array[String]
|
|
301
|
+
def with_subprocess_env: () { () -> untyped } -> untyped
|
|
276
302
|
def spec_files: () -> Array[String]
|
|
277
303
|
def fallback_spec_files: (Subject) -> Array[String]
|
|
278
304
|
def selection_patterns: (Subject) -> Array[String]
|
|
@@ -283,7 +309,6 @@ module Henitai
|
|
|
283
309
|
def relative_candidates: (String, String) -> Array[String]
|
|
284
310
|
def require_candidates: (String, String) -> Array[String]
|
|
285
311
|
def expand_candidates: (String, String) -> Array[String]
|
|
286
|
-
def reap_child: (Integer) -> void
|
|
287
312
|
end
|
|
288
313
|
|
|
289
314
|
class Minitest < Rspec
|
|
@@ -293,11 +318,11 @@ module Henitai
|
|
|
293
318
|
private
|
|
294
319
|
|
|
295
320
|
def run_in_child: (mutant: Mutant, test_files: Array[String], log_paths: Hash[Symbol, String]) -> Integer
|
|
296
|
-
def suite_command: (Array[String]) -> Array[String]
|
|
297
321
|
def run_tests: (Array[String]) -> Integer
|
|
298
322
|
def preload_environment: () -> void
|
|
299
323
|
def setup_load_path: () -> void
|
|
300
324
|
def subprocess_env: () -> Hash[String, String]
|
|
325
|
+
def cleanup_suite_process: (Integer?, untyped) -> void
|
|
301
326
|
def spec_files: () -> Array[String]
|
|
302
327
|
end
|
|
303
328
|
end
|
|
@@ -347,13 +372,29 @@ module Henitai
|
|
|
347
372
|
def fallback_source: (untyped) -> String
|
|
348
373
|
end
|
|
349
374
|
|
|
375
|
+
class CoverageReportReader
|
|
376
|
+
def coverage_lines_by_file: (?String) -> Hash[String, Array[Integer]]
|
|
377
|
+
def test_lines_by_file: (?String) -> Hash[String, Hash[String, Array[Integer]]]
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
class ParallelExecutionRunner
|
|
381
|
+
def initialize: (worker_count: Integer) -> void
|
|
382
|
+
def run: (Array[Mutant], untyped, untyped, untyped, ?Hash[Symbol, untyped]) -> void
|
|
383
|
+
end
|
|
384
|
+
|
|
350
385
|
class StaticFilter
|
|
386
|
+
def initialize: (?coverage_report_reader: CoverageReportReader) -> void
|
|
351
387
|
def apply: (Array[Mutant], untyped) -> Array[Mutant]
|
|
352
388
|
def coverage_lines_for: (untyped) -> Hash[String, Array[Integer]]
|
|
353
389
|
def coverage_lines_by_file: (?String) -> Hash[String, Array[Integer]]
|
|
354
390
|
def test_lines_by_file: (?String) -> Hash[String, Hash[String, Array[Integer]]]
|
|
355
391
|
end
|
|
356
392
|
|
|
393
|
+
class PerTestCoverageSelector
|
|
394
|
+
def initialize: (?coverage_report_reader: CoverageReportReader) -> void
|
|
395
|
+
def filter: (Array[String], Mutant, reports_dir: String) -> Array[String]
|
|
396
|
+
end
|
|
397
|
+
|
|
357
398
|
class ExecutionEngine
|
|
358
399
|
def run: (Array[Mutant], untyped, untyped, ?progress_reporter: untyped) -> Array[Mutant]
|
|
359
400
|
|
|
@@ -363,6 +404,8 @@ module Henitai
|
|
|
363
404
|
def worker_count: (untyped) -> Integer
|
|
364
405
|
def run_linear: (Array[Mutant], untyped, untyped, untyped?, untyped) -> void
|
|
365
406
|
def run_parallel: (Array[Mutant], untyped, untyped, untyped?, untyped) -> void
|
|
407
|
+
def pipe_stdin?: () -> bool
|
|
408
|
+
def start_stdin_watcher: () { () -> void } -> Thread
|
|
366
409
|
def process_mutant: (Mutant, untyped, untyped, untyped?, ?untyped) -> void
|
|
367
410
|
def prioritized_tests_for: (Mutant, untyped, untyped) -> Array[String]
|
|
368
411
|
def test_prioritizer: () -> TestPrioritizer
|
|
@@ -437,6 +480,7 @@ module Henitai
|
|
|
437
480
|
def survived_mutant_header: (Mutant) -> String
|
|
438
481
|
def original_line: (Mutant) -> String
|
|
439
482
|
def mutated_line: (Mutant) -> String
|
|
483
|
+
def display_unparse: (untyped) -> String
|
|
440
484
|
def score_line: (Result) -> String
|
|
441
485
|
def format_row: (String, untyped) -> String
|
|
442
486
|
def count_status: (Result, Symbol) -> Integer
|
|
@@ -507,12 +551,14 @@ module Henitai
|
|
|
507
551
|
include UnparseHelper
|
|
508
552
|
|
|
509
553
|
SCHEMA_VERSION: String
|
|
554
|
+
DEFAULT_THRESHOLDS: Hash[Symbol, Integer]
|
|
510
555
|
|
|
511
556
|
attr_reader mutants: Array[Mutant]
|
|
512
557
|
attr_reader started_at: Time
|
|
513
558
|
attr_reader finished_at: Time
|
|
559
|
+
attr_reader thresholds: Hash[Symbol, Integer]
|
|
514
560
|
|
|
515
|
-
def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time) -> void
|
|
561
|
+
def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time, ?thresholds: Hash[Symbol, Integer]?) -> void
|
|
516
562
|
def killed: () -> Integer
|
|
517
563
|
def survived: () -> Integer
|
|
518
564
|
def equivalent: () -> Integer
|
|
@@ -527,6 +573,7 @@ module Henitai
|
|
|
527
573
|
|
|
528
574
|
def build_files_section: () -> Hash[Symbol, untyped]
|
|
529
575
|
def mutant_to_schema: (Mutant) -> Hash[Symbol, untyped]
|
|
576
|
+
def coverage_schema: (Mutant) -> Hash[Symbol, untyped]
|
|
530
577
|
def replacement_for: (Mutant) -> String
|
|
531
578
|
def location_for: (Mutant) -> Hash[Symbol, untyped]
|
|
532
579
|
def line_column: (Mutant, Symbol) -> Hash[Symbol, Integer]
|
|
@@ -566,19 +613,28 @@ module Henitai
|
|
|
566
613
|
|
|
567
614
|
def build_result: (Array[Mutant], Time, Time) -> Result
|
|
568
615
|
def persist_history: (Result, Time) -> void
|
|
569
|
-
def
|
|
616
|
+
def refresh_coverage_for_targeted_run: (Array[Mutant], Array[String]) -> Array[Mutant]
|
|
617
|
+
def bootstrap_coverage: (Array[String], ?Array[String]?) -> void
|
|
618
|
+
def bootstrap_mutants: (Array[String], Array[Subject]) -> Thread
|
|
619
|
+
def mutants_for: (Array[Subject], Array[String]) -> Array[Mutant]
|
|
620
|
+
def scoped_bootstrap_test_files: (Array[Subject]) -> Array[String]?
|
|
621
|
+
def targeted_run?: () -> bool
|
|
622
|
+
def retry_full_bootstrap?: (Array[Mutant]) -> bool
|
|
570
623
|
def with_reports_dir: () { () -> untyped } -> untyped
|
|
624
|
+
def result_thresholds: () -> Hash[Symbol, Integer]?
|
|
571
625
|
end
|
|
572
626
|
|
|
573
627
|
class CoverageBootstrapper
|
|
574
628
|
def initialize: (?static_filter: StaticFilter) -> void
|
|
575
|
-
def ensure!: (source_files: Array[String], config: Configuration, integration: Integration::Base) -> void
|
|
629
|
+
def ensure!: (source_files: Array[String], config: Configuration, integration: Integration::Base, ?test_files: Array[String]?) -> void
|
|
576
630
|
|
|
577
631
|
private
|
|
578
632
|
|
|
579
633
|
def static_filter: () -> StaticFilter
|
|
580
634
|
def coverage_available?: (Array[String], Configuration) -> bool
|
|
581
|
-
def
|
|
635
|
+
def coverage_fresh?: (Array[String], Configuration, Integration::Base, Array[String]?) -> bool
|
|
636
|
+
def coverage_report_path: (Configuration) -> String
|
|
637
|
+
def bootstrap_coverage: (Integration::Base, Configuration, ?Array[String]?) -> void
|
|
582
638
|
def source_file_paths: (Array[String]) -> Array[String]
|
|
583
639
|
end
|
|
584
640
|
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: henitai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Otten
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-04-14 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: prism
|
|
@@ -88,15 +89,19 @@ files:
|
|
|
88
89
|
- exe/henitai
|
|
89
90
|
- lib/henitai.rb
|
|
90
91
|
- lib/henitai/arid_node_filter.rb
|
|
92
|
+
- lib/henitai/available_cpu_count.rb
|
|
91
93
|
- lib/henitai/cli.rb
|
|
92
94
|
- lib/henitai/configuration.rb
|
|
93
95
|
- lib/henitai/configuration_validator.rb
|
|
94
96
|
- lib/henitai/coverage_bootstrapper.rb
|
|
95
97
|
- lib/henitai/coverage_formatter.rb
|
|
98
|
+
- lib/henitai/coverage_report_reader.rb
|
|
99
|
+
- lib/henitai/eager_load.rb
|
|
96
100
|
- lib/henitai/equivalence_detector.rb
|
|
97
101
|
- lib/henitai/execution_engine.rb
|
|
98
102
|
- lib/henitai/git_diff_analyzer.rb
|
|
99
103
|
- lib/henitai/integration.rb
|
|
104
|
+
- lib/henitai/integration/rspec_process_runner.rb
|
|
100
105
|
- lib/henitai/minitest_simplecov.rb
|
|
101
106
|
- lib/henitai/mutant.rb
|
|
102
107
|
- lib/henitai/mutant/activator.rb
|
|
@@ -113,13 +118,19 @@ files:
|
|
|
113
118
|
- lib/henitai/operators/equality_operator.rb
|
|
114
119
|
- lib/henitai/operators/hash_literal.rb
|
|
115
120
|
- lib/henitai/operators/logical_operator.rb
|
|
121
|
+
- lib/henitai/operators/method_chain_unwrap.rb
|
|
116
122
|
- lib/henitai/operators/method_expression.rb
|
|
117
123
|
- lib/henitai/operators/pattern_match.rb
|
|
118
124
|
- lib/henitai/operators/range_literal.rb
|
|
125
|
+
- lib/henitai/operators/regex_mutator.rb
|
|
119
126
|
- lib/henitai/operators/return_value.rb
|
|
120
127
|
- lib/henitai/operators/safe_navigation.rb
|
|
121
128
|
- lib/henitai/operators/string_literal.rb
|
|
129
|
+
- lib/henitai/operators/unary_operator.rb
|
|
130
|
+
- lib/henitai/operators/update_operator.rb
|
|
131
|
+
- lib/henitai/parallel_execution_runner.rb
|
|
122
132
|
- lib/henitai/parser_current.rb
|
|
133
|
+
- lib/henitai/per_test_coverage_selector.rb
|
|
123
134
|
- lib/henitai/reporter.rb
|
|
124
135
|
- lib/henitai/result.rb
|
|
125
136
|
- lib/henitai/rspec_coverage_formatter.rb
|
|
@@ -147,8 +158,9 @@ metadata:
|
|
|
147
158
|
changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
|
|
148
159
|
documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
|
|
149
160
|
homepage_uri: https://github.com/martinotten/henitai
|
|
150
|
-
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.
|
|
161
|
+
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.4
|
|
151
162
|
rubygems_mfa_required: 'true'
|
|
163
|
+
post_install_message:
|
|
152
164
|
rdoc_options: []
|
|
153
165
|
require_paths:
|
|
154
166
|
- lib
|
|
@@ -163,7 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
163
175
|
- !ruby/object:Gem::Version
|
|
164
176
|
version: '0'
|
|
165
177
|
requirements: []
|
|
166
|
-
rubygems_version:
|
|
178
|
+
rubygems_version: 3.3.5
|
|
179
|
+
signing_key:
|
|
167
180
|
specification_version: 4
|
|
168
181
|
summary: Mutation testing for Ruby
|
|
169
182
|
test_files: []
|