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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2380a5f3e3144cd94e68967bbf73ff7a078608e3004df80222d22646170b28b6
4
- data.tar.gz: 06ee93b8a55d6dd72b12007e66929b2de22e8da984db3f033472868fa6380d92
3
+ metadata.gz: 28b31872e8c6d86e806caa179862bb2af3eda92425e2c9f0690fd4b5d287d573
4
+ data.tar.gz: d7b0d36289b8fbbe11c0a690f1b501f857e738fb997790626282846df5450724
5
5
  SHA512:
6
- metadata.gz: dc9efdf5285729c07f0ebaa6d487a1fc764589522172584468d4fb200ab2a35df706085f933f40a588447c9c599b6939db999ef2bceec42a014a2fe84d336b13
7
- data.tar.gz: 83bc413591f334c60143ee7e8936d5fb0fc42b3e9865f470e20d9b9e85e26c18a9a03e8b3bbac2707fc385e3dd6b2e17dcc4d0599cbce2b5dd11a94704935f54
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.0.2-alpine` image and the Codex CLI preinstalled.
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 == existing_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) || equivalent_logical_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
@@ -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
@@ -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, subjects)
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, subjects)
97
- scoped_tests = scoped_bootstrap_test_files(subjects)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Henitai
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
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], Array[Subject]) -> Thread
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
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: 2026-04-14 00:00:00.000000000 Z
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: 3.3.5
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: []