henitai 0.1.4 → 0.1.5
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 +15 -0
- data/README.md +4 -1
- data/lib/henitai/cli.rb +62 -0
- data/lib/henitai/coverage_bootstrapper.rb +1 -1
- data/lib/henitai/equivalence_detector.rb +59 -1
- data/lib/henitai/integration.rb +4 -0
- data/lib/henitai/runner.rb +3 -46
- data/lib/henitai/version.rb +1 -1
- data/sig/henitai.rbs +1 -5
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28b31872e8c6d86e806caa179862bb2af3eda92425e2c9f0690fd4b5d287d573
|
|
4
|
+
data.tar.gz: d7b0d36289b8fbbe11c0a690f1b501f857e738fb997790626282846df5450724
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed8eab942e02c4723a2a7b77f0d4b713698e61a914e1d9ab8bb3e4b1429514737dfef8160cc279a17d5d6428f6b02910f0d0258cd7e0a61342982d4a2f18e8cc
|
|
7
|
+
data.tar.gz: 4e4994d17183eecf751848d6877a300352f0e366fd715f557bc27fbdeede4e4f9164d51861bf3d1cca475fe2e789d2c80c9e2ff25331f8c34a2e957a59fbe5b9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.5] - 2026-04-14
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Steep type errors in `Runner` after removing targeted-run bootstrap scoping:
|
|
14
|
+
updated `bootstrap_mutants` RBS signature to match the new single-argument
|
|
15
|
+
form and removed stale signatures for `refresh_coverage_for_targeted_run`,
|
|
16
|
+
`scoped_bootstrap_test_files`, `targeted_run?`, and `retry_full_bootstrap?`
|
|
17
|
+
- `RSpec/ExampleLength` offense in `coverage_bootstrapper_spec.rb` — extracted
|
|
18
|
+
workspace setup and report writing into helper methods
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Targeted-run coverage bootstrap no longer scopes the initial run to the
|
|
22
|
+
subject's test files; the full suite is always used for the baseline,
|
|
23
|
+
trading a minor performance optimisation for reliability
|
|
24
|
+
|
|
10
25
|
## [0.1.4] - 2026-04-14
|
|
11
26
|
|
|
12
27
|
### Fixed
|
data/README.md
CHANGED
|
@@ -163,10 +163,13 @@ cd henitai
|
|
|
163
163
|
bundle install
|
|
164
164
|
bundle exec rspec # run tests
|
|
165
165
|
bundle exec rubocop # lint
|
|
166
|
+
bundle exec henitai clean # remove stale generated report artifacts
|
|
166
167
|
bundle exec henitai run # dogfood
|
|
167
168
|
```
|
|
168
169
|
|
|
169
|
-
A Dev Container configuration is included (`.devcontainer/`) for VS Code with the official `ruby:4
|
|
170
|
+
A Dev Container configuration is included (`.devcontainer/`) for VS Code with the official `ruby:4`
|
|
171
|
+
image, the Codex CLI, and RTK preinstalled. Codex support is bootstrapped with
|
|
172
|
+
`rtk init -g --codex --auto-patch` during container creation.
|
|
170
173
|
|
|
171
174
|
## Architecture
|
|
172
175
|
|
data/lib/henitai/cli.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
3
4
|
require "optparse"
|
|
4
5
|
module Henitai
|
|
5
6
|
# Command-line interface entry point.
|
|
@@ -35,6 +36,15 @@ module Henitai
|
|
|
35
36
|
" low: 60"
|
|
36
37
|
].freeze
|
|
37
38
|
|
|
39
|
+
REPORT_CLEANUP_PATHS = [
|
|
40
|
+
%w[mutation-logs baseline.log],
|
|
41
|
+
%w[mutation-logs baseline.stdout.log],
|
|
42
|
+
%w[mutation-logs baseline.stderr.log],
|
|
43
|
+
%w[coverage .resultset.json],
|
|
44
|
+
%w[coverage .last_run.json],
|
|
45
|
+
["henitai_per_test.json"]
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
38
48
|
OPERATOR_METADATA = {
|
|
39
49
|
"ArithmeticOperator" => ["Arithmetic operators", "a + b -> a - b"],
|
|
40
50
|
"EqualityOperator" => ["Comparison operators", "a == b -> a != b"],
|
|
@@ -69,6 +79,7 @@ module Henitai
|
|
|
69
79
|
command = @argv.shift
|
|
70
80
|
case command
|
|
71
81
|
when "run" then run_command
|
|
82
|
+
when "clean" then clean_command
|
|
72
83
|
when "version" then puts Henitai::VERSION
|
|
73
84
|
when "init" then init_command
|
|
74
85
|
when "operator" then operator_command
|
|
@@ -94,6 +105,18 @@ module Henitai
|
|
|
94
105
|
handle_run_error(e)
|
|
95
106
|
end
|
|
96
107
|
|
|
108
|
+
def clean_command
|
|
109
|
+
@command_halted = false
|
|
110
|
+
options = parse_clean_options
|
|
111
|
+
return if @command_halted
|
|
112
|
+
|
|
113
|
+
config = load_config(options)
|
|
114
|
+
removed_paths = cleanup_report_artifacts(config)
|
|
115
|
+
puts clean_summary(removed_paths)
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
handle_run_error(e)
|
|
118
|
+
end
|
|
119
|
+
|
|
97
120
|
def parse_run_options
|
|
98
121
|
options = {}
|
|
99
122
|
build_run_option_parser(options).parse!(@argv)
|
|
@@ -142,6 +165,21 @@ module Henitai
|
|
|
142
165
|
end
|
|
143
166
|
end
|
|
144
167
|
|
|
168
|
+
def parse_clean_options
|
|
169
|
+
options = {}
|
|
170
|
+
build_clean_option_parser(options).parse!(@argv)
|
|
171
|
+
options
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def build_clean_option_parser(options)
|
|
175
|
+
OptionParser.new do |opts|
|
|
176
|
+
opts.banner = "Usage: henitai clean [options]"
|
|
177
|
+
add_config_option(opts, options)
|
|
178
|
+
add_help_option(opts)
|
|
179
|
+
add_version_option(opts)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
145
183
|
def add_since_option(opts, options)
|
|
146
184
|
opts.on("--since GIT_REF", "Only mutate subjects changed since GIT_REF") do |ref|
|
|
147
185
|
options[:since] = ref
|
|
@@ -198,6 +236,7 @@ module Henitai
|
|
|
198
236
|
|
|
199
237
|
Usage:
|
|
200
238
|
henitai run [options] [SUBJECT_PATTERN...]
|
|
239
|
+
henitai clean [options]
|
|
201
240
|
henitai version
|
|
202
241
|
henitai init [PATH]
|
|
203
242
|
henitai operator list
|
|
@@ -207,6 +246,7 @@ module Henitai
|
|
|
207
246
|
bundle exec henitai run --since origin/main
|
|
208
247
|
bundle exec henitai run 'Foo::Bar#my_method'
|
|
209
248
|
bundle exec henitai run 'MyNamespace*' --operators full
|
|
249
|
+
bundle exec henitai clean
|
|
210
250
|
bundle exec henitai init
|
|
211
251
|
bundle exec henitai operator list
|
|
212
252
|
|
|
@@ -239,6 +279,28 @@ module Henitai
|
|
|
239
279
|
exit 2
|
|
240
280
|
end
|
|
241
281
|
|
|
282
|
+
def clean_summary(removed_paths)
|
|
283
|
+
return "No generated report artifacts to clean" if removed_paths.empty?
|
|
284
|
+
|
|
285
|
+
format(
|
|
286
|
+
"Removed %<count>s generated report artifact%<plural>s",
|
|
287
|
+
count: removed_paths.length,
|
|
288
|
+
plural: removed_paths.length == 1 ? "" : "s"
|
|
289
|
+
)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def cleanup_report_artifacts(config)
|
|
293
|
+
removed_paths = report_cleanup_paths(config).select { |path| File.exist?(path) }
|
|
294
|
+
removed_paths.each { |path| FileUtils.rm_f(path) }
|
|
295
|
+
removed_paths
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def report_cleanup_paths(config)
|
|
299
|
+
REPORT_CLEANUP_PATHS.map do |relative_path|
|
|
300
|
+
File.join(config.reports_dir, *relative_path)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
242
304
|
def exit_status_for(result, config)
|
|
243
305
|
result.mutation_score.to_i >= config.thresholds.fetch(:low, 60) ? 0 : 1
|
|
244
306
|
end
|
|
@@ -45,7 +45,7 @@ module Henitai
|
|
|
45
45
|
return covered_sources.any? if existing_sources.empty?
|
|
46
46
|
return covered_sources.any? if test_files
|
|
47
47
|
|
|
48
|
-
covered_sources
|
|
48
|
+
covered_sources.any?
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def coverage_ready?(source_files, config, integration, test_files)
|
|
@@ -19,7 +19,9 @@ module Henitai
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def equivalent_mutation?(mutant)
|
|
22
|
-
equivalent_arithmetic_mutation?(mutant) ||
|
|
22
|
+
equivalent_arithmetic_mutation?(mutant) ||
|
|
23
|
+
equivalent_logical_mutation?(mutant) ||
|
|
24
|
+
equivalent_singleton_equality_mutation?(mutant)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def equivalent_arithmetic_mutation?(mutant)
|
|
@@ -137,6 +139,62 @@ module Henitai
|
|
|
137
139
|
end
|
|
138
140
|
end
|
|
139
141
|
|
|
142
|
+
# Detects `lhs == <singleton>` mutated to `lhs.equal?(<singleton>)` (or the
|
|
143
|
+
# reverse), but only when the receiver is itself a singleton literal.
|
|
144
|
+
#
|
|
145
|
+
# Both sides must be singleton literals so that we can prove, without
|
|
146
|
+
# runtime type information, that `==` reduces to identity comparison.
|
|
147
|
+
# A variable or arbitrary expression as the receiver is unsafe: for example
|
|
148
|
+
# `1.0 == 1` is true while `1.0.equal?(1)` is false, and any object with a
|
|
149
|
+
# custom `#==` can exhibit the same divergence.
|
|
150
|
+
#
|
|
151
|
+
# Singleton types accepted on both receiver and RHS:
|
|
152
|
+
# Symbol – interned; only one instance of :foo ever exists in a process.
|
|
153
|
+
# nil/true/false – singletons by language specification.
|
|
154
|
+
def equivalent_singleton_equality_mutation?(mutant)
|
|
155
|
+
original = mutant.original_node
|
|
156
|
+
mutated = mutant.mutated_node
|
|
157
|
+
|
|
158
|
+
equality_send?(original) && equality_send?(mutated) &&
|
|
159
|
+
same_receiver?(original, mutated) &&
|
|
160
|
+
singleton_literal?(original.children[0]) &&
|
|
161
|
+
singleton_rhs_match?(original, mutated) &&
|
|
162
|
+
equality_operators?(original.children[1], mutated.children[1])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def singleton_rhs_match?(original, mutated)
|
|
166
|
+
rhs = original.children[2]
|
|
167
|
+
singleton_literal?(rhs) && same_node?(rhs, mutated.children[2])
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def equality_send?(node)
|
|
171
|
+
node.is_a?(Parser::AST::Node) &&
|
|
172
|
+
node.type == :send &&
|
|
173
|
+
node.children.size == 3 &&
|
|
174
|
+
equality_operator?(node.children[1])
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def equality_operator?(operator)
|
|
178
|
+
%i[== equal?].include?(operator)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def equality_operators?(op_a, op_b)
|
|
182
|
+
equality_operator?(op_a) && equality_operator?(op_b) && op_a != op_b
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Returns true for AST nodes that represent Ruby singleton values:
|
|
186
|
+
# symbols, nil, true, and false.
|
|
187
|
+
def singleton_literal?(node)
|
|
188
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
189
|
+
|
|
190
|
+
# rubocop:disable Lint/BooleanSymbol
|
|
191
|
+
case node.type
|
|
192
|
+
when :sym, :nil, :true, :false then true
|
|
193
|
+
else false
|
|
194
|
+
end
|
|
195
|
+
# rubocop:enable Lint/BooleanSymbol
|
|
196
|
+
end
|
|
197
|
+
|
|
140
198
|
def same_node?(left, right)
|
|
141
199
|
left == right
|
|
142
200
|
end
|
data/lib/henitai/integration.rb
CHANGED
|
@@ -477,6 +477,10 @@ module Henitai
|
|
|
477
477
|
# runner. Minitest shares selection and execution semantics, but per-test
|
|
478
478
|
# coverage collection is not yet wired into this path.
|
|
479
479
|
class Minitest < Rspec
|
|
480
|
+
def per_test_coverage_supported?
|
|
481
|
+
false
|
|
482
|
+
end
|
|
483
|
+
|
|
480
484
|
def run_mutant(mutant:, test_files:, timeout:)
|
|
481
485
|
setup_load_path
|
|
482
486
|
super
|
data/lib/henitai/runner.rb
CHANGED
|
@@ -41,10 +41,6 @@ module Henitai
|
|
|
41
41
|
# The thread is joined before Gate 3 (static filtering), which is the first
|
|
42
42
|
# phase that requires coverage data.
|
|
43
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
|
-
#
|
|
48
44
|
# @return [Result]
|
|
49
45
|
def run
|
|
50
46
|
started_at = Time.now
|
|
@@ -76,26 +72,15 @@ module Henitai
|
|
|
76
72
|
end
|
|
77
73
|
|
|
78
74
|
def mutants_for(subjects, source_files)
|
|
79
|
-
bootstrap_thread = bootstrap_mutants(source_files
|
|
75
|
+
bootstrap_thread = bootstrap_mutants(source_files)
|
|
80
76
|
mutants = generate_mutants(subjects)
|
|
81
77
|
bootstrap_thread.value
|
|
82
78
|
|
|
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
79
|
filter_mutants(mutants)
|
|
94
80
|
end
|
|
95
81
|
|
|
96
|
-
def bootstrap_mutants(source_files
|
|
97
|
-
|
|
98
|
-
Thread.new { bootstrap_coverage(source_files, scoped_tests) }
|
|
82
|
+
def bootstrap_mutants(source_files)
|
|
83
|
+
Thread.new { bootstrap_coverage(source_files) }
|
|
99
84
|
end
|
|
100
85
|
|
|
101
86
|
def execute_mutants(mutants)
|
|
@@ -131,21 +116,6 @@ module Henitai
|
|
|
131
116
|
@result
|
|
132
117
|
end
|
|
133
118
|
|
|
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
119
|
def bootstrap_coverage(source_files, test_files = nil)
|
|
150
120
|
coverage_bootstrapper.ensure!(source_files:, config:, integration:, test_files:)
|
|
151
121
|
end
|
|
@@ -217,19 +187,6 @@ module Henitai
|
|
|
217
187
|
Array(@subjects)
|
|
218
188
|
end
|
|
219
189
|
|
|
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
|
-
|
|
233
190
|
def unique_subjects(subjects)
|
|
234
191
|
subjects.uniq { |subject| [subject.expression, subject.source_file] }
|
|
235
192
|
end
|
data/lib/henitai/version.rb
CHANGED
data/sig/henitai.rbs
CHANGED
|
@@ -613,13 +613,9 @@ module Henitai
|
|
|
613
613
|
|
|
614
614
|
def build_result: (Array[Mutant], Time, Time) -> Result
|
|
615
615
|
def persist_history: (Result, Time) -> void
|
|
616
|
-
def refresh_coverage_for_targeted_run: (Array[Mutant], Array[String]) -> Array[Mutant]
|
|
617
616
|
def bootstrap_coverage: (Array[String], ?Array[String]?) -> void
|
|
618
|
-
def bootstrap_mutants: (Array[String]
|
|
617
|
+
def bootstrap_mutants: (Array[String]) -> Thread
|
|
619
618
|
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
|
|
623
619
|
def with_reports_dir: () { () -> untyped } -> untyped
|
|
624
620
|
def result_thresholds: () -> Hash[Symbol, Integer]?
|
|
625
621
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Otten
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: prism
|
|
@@ -160,7 +159,6 @@ metadata:
|
|
|
160
159
|
homepage_uri: https://github.com/martinotten/henitai
|
|
161
160
|
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.4
|
|
162
161
|
rubygems_mfa_required: 'true'
|
|
163
|
-
post_install_message:
|
|
164
162
|
rdoc_options: []
|
|
165
163
|
require_paths:
|
|
166
164
|
- lib
|
|
@@ -175,8 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
175
173
|
- !ruby/object:Gem::Version
|
|
176
174
|
version: '0'
|
|
177
175
|
requirements: []
|
|
178
|
-
rubygems_version:
|
|
179
|
-
signing_key:
|
|
176
|
+
rubygems_version: 4.0.6
|
|
180
177
|
specification_version: 4
|
|
181
178
|
summary: Mutation testing for Ruby
|
|
182
179
|
test_files: []
|