henitai 0.3.0 → 0.3.1
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 +27 -1
- data/README.md +2 -1
- data/lib/henitai/integration/scenario_log_support.rb +4 -1
- data/lib/henitai/per_test_coverage.rb +15 -0
- data/lib/henitai/reporter.rb +11 -1
- data/lib/henitai/result.rb +4 -2
- data/lib/henitai/runner.rb +21 -4
- data/lib/henitai/version.rb +1 -1
- data/sig/henitai.rbs +6 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 425e1946a343ce94a9172939a8ad5c5d49e01fe86fbf2b01326b2f0be068df85
|
|
4
|
+
data.tar.gz: 85867b98b26cb3710d0c1e56bd4b37b5ba78f08f47ad8378dc76ae07ce1dfc9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0de6ea6a0a5a830a917f1097ac243ce43c6b8bf3bcc44ac1967133288770746d9c4df8fdee6609081613bfa174be5169e13143a22f1d3d22e8f74fd0cc008d7
|
|
7
|
+
data.tar.gz: 37eedd8f351dbd8a9094c668ce7c56560186936f0953b098f740b6cee1654971d1b5402efe2fd0d5650442b701af7fedaea257ec9f7827575f3f04c052c2593a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.1] - 2026-07-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Per-mutant log capture no longer crashes with
|
|
14
|
+
`Encoding::UndefinedConversionError` when the host app sets
|
|
15
|
+
`Encoding.default_internal` (as Rails test environments do) and a child
|
|
16
|
+
process prints multibyte output: the capped log streams are opened in
|
|
17
|
+
binary mode, so pipe chunks are written byte-for-byte. Also keeps a
|
|
18
|
+
multibyte character split across drain chunks (or truncated at the cap)
|
|
19
|
+
from corrupting the file.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `--since REF` now includes the working tree: tracked files with
|
|
23
|
+
uncommitted changes and untracked files count as changed, matching what
|
|
24
|
+
actually gets tested. Previously only committed changes in `REF..HEAD`
|
|
25
|
+
were considered, so pre-commit runs silently selected nothing.
|
|
26
|
+
- `--since REF` is test-aware: a changed test file selects the source files
|
|
27
|
+
it covers, using the per-test coverage map from the previous run
|
|
28
|
+
(`henitai_per_test.json`). Editing only a test now re-tests the subjects
|
|
29
|
+
that test can kill. First run without a map: no expansion.
|
|
30
|
+
- An empty `--since` run now says why: the terminal summary appends
|
|
31
|
+
`No mutants: no configured source files changed since REF.` instead of
|
|
32
|
+
printing an unexplained all-zero table. `Result` gained a `since`
|
|
33
|
+
attribute to carry the scope.
|
|
34
|
+
|
|
10
35
|
## [0.3.0] - 2026-07-13
|
|
11
36
|
|
|
12
37
|
### Added
|
|
@@ -380,7 +405,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
380
405
|
- CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
|
|
381
406
|
- RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
|
|
382
407
|
|
|
383
|
-
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.3.
|
|
408
|
+
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.3.1...HEAD
|
|
409
|
+
[0.3.1]: https://github.com/martinotten/henitai/compare/v0.3.0...v0.3.1
|
|
384
410
|
[0.3.0]: https://github.com/martinotten/henitai/compare/v0.2.1...v0.3.0
|
|
385
411
|
[0.2.1]: https://github.com/martinotten/henitai/compare/v0.2.0...v0.2.1
|
|
386
412
|
[0.2.0]: https://github.com/martinotten/henitai/compare/v0.1.10...v0.2.0
|
data/README.md
CHANGED
|
@@ -51,7 +51,8 @@ gem install henitai
|
|
|
51
51
|
# Run mutation testing on the entire project
|
|
52
52
|
bundle exec henitai run
|
|
53
53
|
|
|
54
|
-
# Run only on subjects changed since main (CI-friendly)
|
|
54
|
+
# Run only on subjects changed since main (CI-friendly). Includes uncommitted
|
|
55
|
+
# working-tree changes; changed test files select the sources they cover.
|
|
55
56
|
bundle exec henitai run --since origin/main
|
|
56
57
|
|
|
57
58
|
# Run on a specific subject pattern
|
|
@@ -62,8 +62,11 @@ module Henitai
|
|
|
62
62
|
|
|
63
63
|
# One capture channel: a pipe whose read end is drained (capped) into the
|
|
64
64
|
# log file by a dedicated thread. Returns the pieces close needs.
|
|
65
|
+
# Binary mode: readpartial yields ASCII-8BIT chunks, and a text-mode file
|
|
66
|
+
# would transcode them (raising Encoding::UndefinedConversionError on
|
|
67
|
+
# multibyte output when Encoding.default_internal is set, as Rails does).
|
|
65
68
|
def open_capped_stream(path, cap)
|
|
66
|
-
file = File.new(path, "
|
|
69
|
+
file = File.new(path, "wb")
|
|
67
70
|
file.sync = true
|
|
68
71
|
reader, writer = IO.pipe
|
|
69
72
|
thread = Thread.new { drain_capped(reader, file, cap) }
|
|
@@ -28,6 +28,21 @@ module Henitai
|
|
|
28
28
|
map.keys.select { |test| covers?(test, mutant) }.sort
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# The source files the given test file's recorded coverage reaches.
|
|
32
|
+
# Test keys are matched path-insensitively (relative or absolute form);
|
|
33
|
+
# returned source paths are absolute, as recorded.
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<String>] sorted; empty when the test is unknown or the
|
|
36
|
+
# map is unavailable.
|
|
37
|
+
def source_files_covered_by(test_file)
|
|
38
|
+
wanted = File.expand_path(test_file.to_s)
|
|
39
|
+
map.select { |test, _| File.expand_path(test) == wanted }
|
|
40
|
+
.values
|
|
41
|
+
.flat_map(&:keys)
|
|
42
|
+
.uniq
|
|
43
|
+
.sort
|
|
44
|
+
end
|
|
45
|
+
|
|
31
46
|
# True when the given test file's recorded coverage intersects the
|
|
32
47
|
# mutant's current line range.
|
|
33
48
|
def covers?(test, mutant)
|
data/lib/henitai/reporter.rb
CHANGED
|
@@ -137,10 +137,20 @@ module Henitai
|
|
|
137
137
|
format_row("No coverage", count_status(result, :no_coverage)),
|
|
138
138
|
format_row("Duration", format_duration(result.duration)),
|
|
139
139
|
reused_verdicts_line(result),
|
|
140
|
-
executed_only_score_line(result)
|
|
140
|
+
executed_only_score_line(result),
|
|
141
|
+
empty_since_scope_line(result)
|
|
141
142
|
].compact
|
|
142
143
|
end
|
|
143
144
|
|
|
145
|
+
# A zero-mutant summary with every counter at 0 and n/a scores is
|
|
146
|
+
# baffling without its cause; name it when the run was scoped by --since.
|
|
147
|
+
def empty_since_scope_line(result)
|
|
148
|
+
return nil unless result.mutants.empty?
|
|
149
|
+
return nil unless result.respond_to?(:since) && result.since
|
|
150
|
+
|
|
151
|
+
"No mutants: no configured source files changed since #{result.since}."
|
|
152
|
+
end
|
|
153
|
+
|
|
144
154
|
def reused_verdicts_line(result)
|
|
145
155
|
reused = reused_mutants(result)
|
|
146
156
|
return nil if reused.empty?
|
data/lib/henitai/result.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Henitai
|
|
|
16
16
|
DEFAULT_THRESHOLDS = { high: 80, low: 60 }.freeze
|
|
17
17
|
|
|
18
18
|
attr_reader :mutants, :started_at, :finished_at, :thresholds, :survivor_stats,
|
|
19
|
-
:session_id, :git_sha
|
|
19
|
+
:session_id, :git_sha, :since
|
|
20
20
|
|
|
21
21
|
# @param source_provider [#call] maps a file path to its source string.
|
|
22
22
|
# Injected so the domain object performs no disk IO; the caller (which
|
|
@@ -32,7 +32,8 @@ module Henitai
|
|
|
32
32
|
def initialize(mutants:, started_at:, finished_at:, thresholds: nil,
|
|
33
33
|
partial_rerun: false, survivor_stats: nil,
|
|
34
34
|
session_id: SecureRandom.uuid, git_sha: nil,
|
|
35
|
-
source_provider: ->(_file) { "" }, authoritative: true
|
|
35
|
+
source_provider: ->(_file) { "" }, authoritative: true,
|
|
36
|
+
since: nil)
|
|
36
37
|
@mutants = mutants
|
|
37
38
|
@started_at = started_at
|
|
38
39
|
@finished_at = finished_at
|
|
@@ -43,6 +44,7 @@ module Henitai
|
|
|
43
44
|
@git_sha = git_sha
|
|
44
45
|
@source_provider = source_provider
|
|
45
46
|
@authoritative = authoritative
|
|
47
|
+
@since = since
|
|
46
48
|
end
|
|
47
49
|
# rubocop:enable Metrics/ParameterLists
|
|
48
50
|
|
data/lib/henitai/runner.rb
CHANGED
|
@@ -168,7 +168,8 @@ module Henitai
|
|
|
168
168
|
survivor_stats: survivor_strategy.survivor_stats,
|
|
169
169
|
git_sha: safe_head_sha,
|
|
170
170
|
source_provider: source_provider,
|
|
171
|
-
authoritative: full_run
|
|
171
|
+
authoritative: full_run?,
|
|
172
|
+
since: @since
|
|
172
173
|
)
|
|
173
174
|
end
|
|
174
175
|
|
|
@@ -267,12 +268,28 @@ module Henitai
|
|
|
267
268
|
def filter_changed(files)
|
|
268
269
|
return files unless @since
|
|
269
270
|
|
|
270
|
-
changed_file_set =
|
|
271
|
-
|
|
272
|
-
.map { |path| normalize_path(path) }
|
|
271
|
+
changed_file_set = changed_paths_since.map { |path| normalize_path(path) }
|
|
272
|
+
changed_file_set += covered_sources_for_changed_tests(changed_file_set)
|
|
273
273
|
files.select { |path| changed_file_set.include?(normalize_path(path)) }
|
|
274
274
|
end
|
|
275
275
|
|
|
276
|
+
# Committed changes since the ref plus the current working tree (tracked
|
|
277
|
+
# dirty and untracked files): the working tree is what gets tested, so it
|
|
278
|
+
# is always part of "changed since REF".
|
|
279
|
+
def changed_paths_since
|
|
280
|
+
git_diff_analyzer.changed_files(from: @since, to: "HEAD") +
|
|
281
|
+
git_diff_analyzer.working_tree_changed_files
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Changed test files select the source files they cover, so an edited
|
|
285
|
+
# test re-tests the subjects it can kill. Uses the per-test map from the
|
|
286
|
+
# previous run — Gate 1 runs before this run's bootstrap finishes.
|
|
287
|
+
def covered_sources_for_changed_tests(changed_paths)
|
|
288
|
+
changed_paths
|
|
289
|
+
.flat_map { |path| per_test_coverage.source_files_covered_by(path) }
|
|
290
|
+
.map { |path| normalize_path(path) }
|
|
291
|
+
end
|
|
292
|
+
|
|
276
293
|
def pattern_subjects
|
|
277
294
|
Array(@subjects)
|
|
278
295
|
end
|
data/lib/henitai/version.rb
CHANGED
data/sig/henitai.rbs
CHANGED
|
@@ -718,6 +718,7 @@ module Henitai
|
|
|
718
718
|
def initialize: (reports_dir: String, ?coverage_report_reader: CoverageReportReader) -> void
|
|
719
719
|
def available?: () -> bool
|
|
720
720
|
def tests_covering: (untyped) -> Array[String]
|
|
721
|
+
def source_files_covered_by: (untyped) -> Array[String]
|
|
721
722
|
def covers?: (String, untyped) -> bool
|
|
722
723
|
|
|
723
724
|
private
|
|
@@ -852,6 +853,7 @@ module Henitai
|
|
|
852
853
|
def reused_verdicts_line: (Result) -> String?
|
|
853
854
|
def reused_mutants: (Result) -> Array[Mutant]
|
|
854
855
|
def executed_only_score_line: (Result) -> String?
|
|
856
|
+
def empty_since_scope_line: (Result) -> String?
|
|
855
857
|
def count_status: (Result, Symbol) -> Integer
|
|
856
858
|
def format_duration: (Float) -> String
|
|
857
859
|
def format_percent: (Float?) -> String
|
|
@@ -977,8 +979,9 @@ module Henitai
|
|
|
977
979
|
attr_reader survivor_stats: Hash[Symbol, untyped]?
|
|
978
980
|
attr_reader session_id: String
|
|
979
981
|
attr_reader git_sha: String?
|
|
982
|
+
attr_reader since: String?
|
|
980
983
|
|
|
981
|
-
def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time, ?thresholds: Hash[Symbol, Integer]?, ?partial_rerun: bool, ?survivor_stats: Hash[Symbol, untyped]?, ?session_id: String, ?git_sha: String?, ?source_provider: ^(String) -> String, ?authoritative: bool) -> void
|
|
984
|
+
def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time, ?thresholds: Hash[Symbol, Integer]?, ?partial_rerun: bool, ?survivor_stats: Hash[Symbol, untyped]?, ?session_id: String, ?git_sha: String?, ?source_provider: ^(String) -> String, ?authoritative: bool, ?since: String?) -> void
|
|
982
985
|
def partial_rerun?: () -> bool
|
|
983
986
|
def authoritative?: () -> bool
|
|
984
987
|
def killed: () -> Integer
|
|
@@ -1086,6 +1089,8 @@ module Henitai
|
|
|
1086
1089
|
def reject_excluded: (Array[String]) -> Array[String]
|
|
1087
1090
|
def excluded_source_files: () -> Array[String]
|
|
1088
1091
|
def filter_changed: (Array[String]) -> Array[String]
|
|
1092
|
+
def changed_paths_since: () -> Array[String]
|
|
1093
|
+
def covered_sources_for_changed_tests: (Array[String]) -> Array[String]
|
|
1089
1094
|
def pattern_subjects: () -> Array[Subject]
|
|
1090
1095
|
def unique_subjects: (Array[Subject]) -> Array[Subject]
|
|
1091
1096
|
def normalize_path: (String) -> String
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: henitai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Otten
|
|
@@ -223,7 +223,7 @@ metadata:
|
|
|
223
223
|
changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
|
|
224
224
|
documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
|
|
225
225
|
homepage_uri: https://github.com/martinotten/henitai
|
|
226
|
-
source_code_uri: https://github.com/martinotten/henitai/tree/v0.3.
|
|
226
|
+
source_code_uri: https://github.com/martinotten/henitai/tree/v0.3.1
|
|
227
227
|
rubygems_mfa_required: 'true'
|
|
228
228
|
rdoc_options: []
|
|
229
229
|
require_paths:
|