henitai 0.1.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 748dc8e07fefafa1e15d8f84b6e047896d20615362c1edc4d69e6321acd3db4d
4
- data.tar.gz: d61c9559adc952e63417d3edffc9826093ea5e0e9b3298761badf85080e11a12
3
+ metadata.gz: 2380a5f3e3144cd94e68967bbf73ff7a078608e3004df80222d22646170b28b6
4
+ data.tar.gz: 06ee93b8a55d6dd72b12007e66929b2de22e8da984db3f033472868fa6380d92
5
5
  SHA512:
6
- metadata.gz: 3172df8aa6dac29dd8498e0c2b697e0a1698a049ce86db1b976a5d93163206e146b16b2a641dd77088e0cc58e17238ffa657f704732a0e574d93a331faa80be0
7
- data.tar.gz: 6458897e5a6c53321362b5f65cfd9018c8ca415ccad74a0ba8303c56336e182407b50cd3ac02d976bb55d2eb284444676de137719194c7bbb04f20717903f082
6
+ metadata.gz: dc9efdf5285729c07f0ebaa6d487a1fc764589522172584468d4fb200ab2a35df706085f933f40a588447c9c599b6939db999ef2bceec42a014a2fe84d336b13
7
+ data.tar.gz: 83bc413591f334c60143ee7e8936d5fb0fc42b3e9865f470e20d9b9e85e26c18a9a03e8b3bbac2707fc385e3dd6b2e17dcc4d0599cbce2b5dd11a94704935f54
data/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.4] - 2026-04-14
11
+
12
+ ### Fixed
13
+ - `StringLiteral` operator no longer generates no-op mutations where the
14
+ replacement equals the original value (e.g. the spurious `"" → ""` mutant
15
+ that was emitted for methods already returning an empty string literal)
16
+ - Terminal diff output now uses `display_unparse` for string literal nodes,
17
+ making whitespace-only mutations unambiguous in the report
18
+ (e.g. `""`, `" "`, and `"\n"` are now visually distinct)
19
+ - Targeted coverage bootstrap (`--since` / explicit subjects) now correctly
20
+ retriggers a full suite run when the scoped bootstrap does not produce
21
+ coverage for all configured source files; previously the run could raise
22
+ `CoverageError` even though a fallback was available
23
+ - Coverage formatter specs now honor `HENITAI_REPORTS_DIR`, so the baseline
24
+ coverage bootstrap no longer fails when the suite runs under the mutation
25
+ runner's configured reports directory
26
+
27
+ ### Changed
28
+ - `ScenarioExecutionResult.build` factory method consolidates status and
29
+ exit-status derivation that was previously spread across `Integration`,
30
+ reducing the mutation surface of the value object
31
+
10
32
  ## [0.1.3] - 2026-04-13
11
33
 
12
34
  ### Added
@@ -122,7 +144,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
122
144
  - CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
123
145
  - RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
124
146
 
125
- [Unreleased]: https://github.com/martinotten/henitai/compare/v0.1.3...HEAD
147
+ [Unreleased]: https://github.com/martinotten/henitai/compare/v0.1.4...HEAD
148
+ [0.1.4]: https://github.com/martinotten/henitai/compare/v0.1.3...v0.1.4
126
149
  [0.1.3]: https://github.com/martinotten/henitai/compare/v0.1.2...v0.1.3
127
150
  [0.1.2]: https://github.com/martinotten/henitai/compare/v0.1.1...v0.1.2
128
151
  [0.1.1]: https://github.com/martinotten/henitai/compare/v0.1.0...v0.1.1
@@ -27,7 +27,7 @@ module Henitai
27
27
  bootstrap_coverage(integration, config, test_files)
28
28
  end
29
29
 
30
- return if coverage_available?(source_files, config)
30
+ return if coverage_available?(source_files, config, test_files)
31
31
 
32
32
  raise CoverageError,
33
33
  "Coverage data is unavailable for the configured source files"
@@ -37,24 +37,37 @@ module Henitai
37
37
 
38
38
  attr_reader :static_filter
39
39
 
40
- def coverage_available?(source_files, config)
40
+ def coverage_available?(source_files, config, test_files)
41
41
  coverage_lines = static_filter.coverage_lines_for(config)
42
+ covered_sources = covered_source_files(source_files, coverage_lines)
43
+ existing_sources = existing_source_file_paths(source_files)
42
44
 
43
- source_file_paths(source_files).any? do |path|
44
- Array(coverage_lines[path]).any?
45
- end
45
+ return covered_sources.any? if existing_sources.empty?
46
+ return covered_sources.any? if test_files
47
+
48
+ covered_sources == existing_sources
46
49
  end
47
50
 
48
51
  def coverage_ready?(source_files, config, integration, test_files)
49
52
  coverage_fresh?(source_files, config, integration, test_files) &&
50
- coverage_available?(source_files, config) &&
53
+ coverage_available?(source_files, config, test_files) &&
51
54
  per_test_coverage_ready?(source_files, config, integration, test_files)
52
55
  end
53
56
 
57
+ def covered_source_files(source_files, coverage_lines)
58
+ source_file_paths(source_files).select do |path|
59
+ Array(coverage_lines[path]).any?
60
+ end
61
+ end
62
+
54
63
  def source_file_paths(source_files)
55
64
  Array(source_files).map { |path| File.expand_path(path) }
56
65
  end
57
66
 
67
+ def existing_source_file_paths(source_files)
68
+ source_file_paths(source_files).select { |path| File.exist?(path) }
69
+ end
70
+
58
71
  # Returns true when a coverage report already exists and is newer than
59
72
  # every watched source and test file. Stale or absent reports return false.
60
73
  def coverage_fresh?(source_files, config, integration, test_files)
@@ -314,35 +314,18 @@ module Henitai
314
314
  end
315
315
 
316
316
  def build_result(wait_result, log_paths)
317
- status = scenario_status(wait_result)
318
317
  stdout = read_log_file(log_paths[:stdout_path])
319
318
  stderr = read_log_file(log_paths[:stderr_path])
320
319
  write_combined_log(log_paths[:log_path], stdout, stderr)
321
320
 
322
- ScenarioExecutionResult.new(
323
- status:,
321
+ ScenarioExecutionResult.build(
322
+ wait_result:,
324
323
  stdout:,
325
324
  stderr:,
326
- log_path: log_paths[:log_path],
327
- exit_status: exit_status_for(wait_result)
325
+ log_path: log_paths[:log_path]
328
326
  )
329
327
  end
330
328
 
331
- def scenario_status(wait_result)
332
- return :timeout if wait_result == :timeout
333
- return :compile_error if exit_status_for(wait_result) == 2
334
- return :survived if wait_result.respond_to?(:success?) && wait_result.success?
335
-
336
- :killed
337
- end
338
-
339
- def exit_status_for(wait_result)
340
- return nil if wait_result == :timeout
341
- return nil unless wait_result.respond_to?(:exitstatus)
342
-
343
- wait_result.exitstatus
344
- end
345
-
346
329
  def spec_files
347
330
  Dir.glob("spec/**/*_spec.rb")
348
331
  end
@@ -27,7 +27,8 @@ module Henitai
27
27
  private
28
28
 
29
29
  def mutate_plain_string(node, subject:)
30
- REPLACEMENTS.map do |replacement|
30
+ original_value = node.children.first
31
+ REPLACEMENTS.reject { |r| r == original_value }.map do |replacement|
31
32
  build_mutant(
32
33
  subject:,
33
34
  original_node: node,
@@ -126,11 +126,23 @@ module Henitai
126
126
  end
127
127
 
128
128
  def original_line(mutant)
129
- format("- %s", safe_unparse(mutant.original_node))
129
+ format("- %s", display_unparse(mutant.original_node))
130
130
  end
131
131
 
132
132
  def mutated_line(mutant)
133
- format("+ %s", safe_unparse(mutant.mutated_node))
133
+ format("+ %s", display_unparse(mutant.mutated_node))
134
+ end
135
+
136
+ # Like safe_unparse but makes invisible characters visible in terminal
137
+ # output. For string literal nodes the inner value is shown via #inspect
138
+ # so that e.g. "" vs " " vs "\n" are unambiguous. Other nodes unparse
139
+ # normally.
140
+ def display_unparse(node)
141
+ if node.respond_to?(:type) && node.respond_to?(:children) && node.type == :str
142
+ node.children.first.inspect
143
+ else
144
+ safe_unparse(node)
145
+ end
134
146
  end
135
147
 
136
148
  def score_line(result)
@@ -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: false)
64
+ def should_show_logs?(all_logs: nil)
55
65
  all_logs || timeout?
56
66
  end
57
67
 
58
- def failure_tail(all_logs: false, lines: 12)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Henitai
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
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
@@ -474,6 +480,7 @@ module Henitai
474
480
  def survived_mutant_header: (Mutant) -> String
475
481
  def original_line: (Mutant) -> String
476
482
  def mutated_line: (Mutant) -> String
483
+ def display_unparse: (untyped) -> String
477
484
  def score_line: (Result) -> String
478
485
  def format_row: (String, untyped) -> String
479
486
  def count_status: (Result, Symbol) -> Integer
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: henitai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Otten
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-13 00:00:00.000000000 Z
11
+ date: 2026-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prism
@@ -158,7 +158,7 @@ metadata:
158
158
  changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
159
159
  documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
160
160
  homepage_uri: https://github.com/martinotten/henitai
161
- source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.2
161
+ source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.4
162
162
  rubygems_mfa_required: 'true'
163
163
  post_install_message:
164
164
  rdoc_options: []