henitai 0.3.0 → 0.4.0
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 +45 -1
- data/README.md +12 -1
- data/assets/schema/henitai.schema.json +1 -1
- data/lib/henitai/cli/operator_command.rb +2 -1
- data/lib/henitai/cli/run_options.rb +1 -1
- data/lib/henitai/configuration_validator.rb +1 -1
- data/lib/henitai/integration/scenario_log_support.rb +4 -1
- data/lib/henitai/operator.rb +12 -2
- data/lib/henitai/operators/hash_key_type.rb +50 -0
- data/lib/henitai/operators/hash_literal.rb +19 -20
- data/lib/henitai/operators.rb +1 -0
- 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 +8 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 458245aa72b025224bd6c95c2bcca98ea095dd86a41e93ab8f0671ba56644369
|
|
4
|
+
data.tar.gz: 94a822238b18f41803753a85adae77675f422d2a901cdd89723ea68b14a8842a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed70e5425023aa08cd3ecd4ff6cb20dbd6c78b14f1811caaf9b43034e7649cdfeed617398a4fbf200730c1f4e8a36ab1fdfbe727b27379f4ed3b4eeef53be781
|
|
7
|
+
data.tar.gz: 0f2b4a04643f9e1bcd3f188e06d851f34eafbbf65dcafd42b9698678cb0ee540fca70e31207003d81cb4713ee1ca2c78901780e5f474128e20331c150949052a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-07-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `hard` operator set (`mutation.operators: hard` / `--operators hard`), a
|
|
14
|
+
strict superset of `full` for usually-unkillable mutations (ADR-12):
|
|
15
|
+
currently `EqualityIdentityOperator` and the new `HashKeyType`
|
|
16
|
+
(`{ a: 1 }` -> `{ "a" => 1 }`) — framework key normalization (e.g.
|
|
17
|
+
ActiveRecord `order`/`where`) makes key-type mutants frequently equivalent
|
|
18
|
+
- `HashLiteral` gains per-pair removal (`{ a: 1, b: 2 }` -> `{ b: 2 }`);
|
|
19
|
+
single-pair hashes and double-splat entries are skipped
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `full` now means "usually killable": the symbol-key -> string-key mutation
|
|
23
|
+
moved from `HashLiteral` into the new hard-set `HashKeyType`, and
|
|
24
|
+
`EqualityIdentityOperator` moved from `full` to `hard` — `full` runs emit
|
|
25
|
+
fewer, higher-signal mutants
|
|
26
|
+
|
|
27
|
+
## [0.3.1] - 2026-07-13
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Per-mutant log capture no longer crashes with
|
|
31
|
+
`Encoding::UndefinedConversionError` when the host app sets
|
|
32
|
+
`Encoding.default_internal` (as Rails test environments do) and a child
|
|
33
|
+
process prints multibyte output: the capped log streams are opened in
|
|
34
|
+
binary mode, so pipe chunks are written byte-for-byte. Also keeps a
|
|
35
|
+
multibyte character split across drain chunks (or truncated at the cap)
|
|
36
|
+
from corrupting the file.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- `--since REF` now includes the working tree: tracked files with
|
|
40
|
+
uncommitted changes and untracked files count as changed, matching what
|
|
41
|
+
actually gets tested. Previously only committed changes in `REF..HEAD`
|
|
42
|
+
were considered, so pre-commit runs silently selected nothing.
|
|
43
|
+
- `--since REF` is test-aware: a changed test file selects the source files
|
|
44
|
+
it covers, using the per-test coverage map from the previous run
|
|
45
|
+
(`henitai_per_test.json`). Editing only a test now re-tests the subjects
|
|
46
|
+
that test can kill. First run without a map: no expansion.
|
|
47
|
+
- An empty `--since` run now says why: the terminal summary appends
|
|
48
|
+
`No mutants: no configured source files changed since REF.` instead of
|
|
49
|
+
printing an unexplained all-zero table. `Result` gained a `since`
|
|
50
|
+
attribute to carry the scope.
|
|
51
|
+
|
|
10
52
|
## [0.3.0] - 2026-07-13
|
|
11
53
|
|
|
12
54
|
### Added
|
|
@@ -380,7 +422,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
380
422
|
- CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
|
|
381
423
|
- RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
|
|
382
424
|
|
|
383
|
-
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.
|
|
425
|
+
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.4.0...HEAD
|
|
426
|
+
[0.4.0]: https://github.com/martinotten/henitai/compare/v0.3.1...v0.4.0
|
|
427
|
+
[0.3.1]: https://github.com/martinotten/henitai/compare/v0.3.0...v0.3.1
|
|
384
428
|
[0.3.0]: https://github.com/martinotten/henitai/compare/v0.2.1...v0.3.0
|
|
385
429
|
[0.2.1]: https://github.com/martinotten/henitai/compare/v0.2.0...v0.2.1
|
|
386
430
|
[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
|
|
@@ -270,7 +271,17 @@ The repository ships a JSON Schema at [`assets/schema/henitai.schema.json`](/wor
|
|
|
270
271
|
- `AssignmentExpression` — mutate compound assignment
|
|
271
272
|
- `UnaryOperator` — remove unary `-` and `~`
|
|
272
273
|
- `UpdateOperator` — swap compound assignments (`+=`↔`-=`, `*=`↔`/=`, `||=`↔`&&=`)
|
|
274
|
+
|
|
275
|
+
**Hard** — adds usually-unkillable mutations on top of full, for hunting the
|
|
276
|
+
last survivors (see [ADR-12](docs/architecture/adr/ADR-12-hard-operator-set.md)):
|
|
277
|
+
|
|
273
278
|
- `EqualityIdentityOperator` — `==` ↔ `eql?`/`equal?` (hardest equality pairing to kill; see [ADR-10](docs/architecture/adr/ADR-10-split-equality-identity-mutations.md))
|
|
279
|
+
- `HashKeyType` — `{ a: 1 }` → `{ "a" => 1 }` (frameworks that normalize key
|
|
280
|
+
types, e.g. ActiveRecord `order`/`where`, make these mutants equivalent at
|
|
281
|
+
many call sites; disable per site with `# henitai:disable HashKeyType`)
|
|
282
|
+
|
|
283
|
+
`HashLiteral` (full set) empties the hash and removes one pair at a time
|
|
284
|
+
(`{ a: 1, b: 2 }` → `{}` / `{ b: 2 }`).
|
|
274
285
|
|
|
275
286
|
## Stryker Dashboard integration
|
|
276
287
|
|
|
@@ -14,7 +14,8 @@ module Henitai
|
|
|
14
14
|
"StringLiteral" => ["String literals", '"foo" -> ""'],
|
|
15
15
|
"ReturnValue" => ["Return expressions", "return x -> return nil"],
|
|
16
16
|
"ArrayDeclaration" => ["Array literals", "[1, 2] -> []"],
|
|
17
|
-
"HashLiteral" => ["Hash literals", "{ a: 1 } -> {}"],
|
|
17
|
+
"HashLiteral" => ["Hash literals", "{ a: 1, b: 2 } -> {} / { b: 2 }"],
|
|
18
|
+
"HashKeyType" => ["Hash key types", '{ a: 1 } -> { "a" => 1 }'],
|
|
18
19
|
"RangeLiteral" => ["Range literals", "1..5 -> 1...5"],
|
|
19
20
|
"SafeNavigation" => ["Safe navigation", "user&.name -> user.name"],
|
|
20
21
|
"PatternMatch" => ["Pattern matching", "in { x: Integer } -> in { x: String }"],
|
|
@@ -33,7 +33,7 @@ module Henitai
|
|
|
33
33
|
VALID_THRESHOLDS_KEYS = %i[high low].freeze
|
|
34
34
|
VALID_DASHBOARD_KEYS = %i[project base_url].freeze
|
|
35
35
|
VALID_INTEGRATION_KEYS = %i[name].freeze
|
|
36
|
-
VALID_OPERATORS = %i[light full].freeze
|
|
36
|
+
VALID_OPERATORS = %i[light full hard].freeze
|
|
37
37
|
VALIDATION_STEPS = %i[
|
|
38
38
|
validate_top_level_keys
|
|
39
39
|
validate_integration
|
|
@@ -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) }
|
data/lib/henitai/operator.rb
CHANGED
|
@@ -44,13 +44,23 @@ module Henitai
|
|
|
44
44
|
AssignmentExpression
|
|
45
45
|
UnaryOperator
|
|
46
46
|
UpdateOperator
|
|
47
|
+
]).freeze
|
|
48
|
+
|
|
49
|
+
# Usually-unkillable operators on top of full: mutations whose survival
|
|
50
|
+
# rarely indicates a test gap (framework key normalization, the
|
|
51
|
+
# ==/eql?/equal? pairing). Opt in when hunting the last survivors
|
|
52
|
+
# (ADR-12).
|
|
53
|
+
HARD_SET = (FULL_SET + %w[
|
|
47
54
|
EqualityIdentityOperator
|
|
55
|
+
HashKeyType
|
|
48
56
|
]).freeze
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
SETS = { light: LIGHT_SET, full: FULL_SET, hard: HARD_SET }.freeze
|
|
59
|
+
|
|
60
|
+
# @param set [Symbol] :light, :full or :hard
|
|
51
61
|
# @return [Array<Operator>] operator instances for the given set
|
|
52
62
|
def self.for_set(set)
|
|
53
|
-
names = set.to_sym
|
|
63
|
+
names = SETS.fetch(set.to_sym, LIGHT_SET)
|
|
54
64
|
names.map { |name| Henitai::Operators.const_get(name).new }
|
|
55
65
|
end
|
|
56
66
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
module Operators
|
|
5
|
+
# Mutates symbol hash keys into string keys, one pair at a time
|
|
6
|
+
# (`{ a: 1 }` -> `{ "a" => 1 }`). Symbol/string key confusion is a real
|
|
7
|
+
# defect class, but frameworks that normalize keys (e.g. ActiveRecord's
|
|
8
|
+
# `order`/`where`) make these mutants frequently unkillable — hence the
|
|
9
|
+
# hard set, not full (ADR-12).
|
|
10
|
+
class HashKeyType < Henitai::Operator
|
|
11
|
+
NODE_TYPES = [:hash].freeze
|
|
12
|
+
|
|
13
|
+
def self.node_types
|
|
14
|
+
NODE_TYPES
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def mutate(node, subject:)
|
|
18
|
+
node.children.each_with_index.filter_map do |pair, index|
|
|
19
|
+
next unless symbol_key_pair?(pair)
|
|
20
|
+
|
|
21
|
+
build_mutant(
|
|
22
|
+
subject:,
|
|
23
|
+
original_node: node,
|
|
24
|
+
mutated_node: mutated_hash(node, index),
|
|
25
|
+
description: "replaced symbol key with string key"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def symbol_key_pair?(node)
|
|
33
|
+
node.type == :pair && node.children.first&.type == :sym
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def mutated_hash(node, index)
|
|
37
|
+
pairs = node.children.each_with_index.map do |pair, pair_index|
|
|
38
|
+
pair_index == index ? stringified_pair(pair) : pair
|
|
39
|
+
end
|
|
40
|
+
Parser::AST::Node.new(:hash, pairs)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stringified_pair(pair)
|
|
44
|
+
key, value = pair.children
|
|
45
|
+
string_key = Parser::AST::Node.new(:str, [key.children.first.to_s])
|
|
46
|
+
Parser::AST::Node.new(:pair, [string_key, value])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -4,7 +4,10 @@ require_relative "../parser_current"
|
|
|
4
4
|
|
|
5
5
|
module Henitai
|
|
6
6
|
module Operators
|
|
7
|
-
# Reduces hash literals
|
|
7
|
+
# Reduces hash literals: empties the whole hash and removes one pair at a
|
|
8
|
+
# time. Symbol-key -> string-key mutation lives in {HashKeyType} (hard
|
|
9
|
+
# set) because framework key normalization makes it frequently unkillable
|
|
10
|
+
# (ADR-12).
|
|
8
11
|
class HashLiteral < Henitai::Operator
|
|
9
12
|
NODE_TYPES = [:hash].freeze
|
|
10
13
|
|
|
@@ -16,7 +19,7 @@ module Henitai
|
|
|
16
19
|
return [] if node.children.empty?
|
|
17
20
|
|
|
18
21
|
mutants = [empty_hash_mutant(node, subject:)]
|
|
19
|
-
mutants.concat(
|
|
22
|
+
mutants.concat(pair_removal_mutants(node, subject:))
|
|
20
23
|
mutants
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -31,35 +34,31 @@ module Henitai
|
|
|
31
34
|
)
|
|
32
35
|
end
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
# Removing the only entry would duplicate the empty-hash mutant.
|
|
38
|
+
def pair_removal_mutants(node, subject:)
|
|
39
|
+
return [] if node.children.size < 2
|
|
40
|
+
|
|
35
41
|
node.children.each_with_index.filter_map do |pair, index|
|
|
36
|
-
next unless
|
|
42
|
+
next unless pair.type == :pair
|
|
37
43
|
|
|
38
44
|
build_mutant(
|
|
39
45
|
subject:,
|
|
40
46
|
original_node: node,
|
|
41
|
-
mutated_node:
|
|
42
|
-
description: "
|
|
47
|
+
mutated_node: hash_without_pair(node, index),
|
|
48
|
+
description: "removed hash pair #{pair_key_label(pair)}"
|
|
43
49
|
)
|
|
44
50
|
end
|
|
45
51
|
end
|
|
46
52
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def mutated_hash(node, pair_index)
|
|
52
|
-
mutated_pairs = node.children.each_with_index.map do |pair, index|
|
|
53
|
-
index == pair_index ? mutated_pair(pair) : pair
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
Parser::AST::Node.new(:hash, mutated_pairs)
|
|
53
|
+
def hash_without_pair(node, pair_index)
|
|
54
|
+
remaining = node.children.reject.with_index { |_pair, index| index == pair_index }
|
|
55
|
+
Parser::AST::Node.new(:hash, remaining)
|
|
57
56
|
end
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
# Pair keys are always AST nodes (sym/str/…); their first child is the
|
|
59
|
+
# literal value used purely as a human-readable label.
|
|
60
|
+
def pair_key_label(pair)
|
|
61
|
+
pair.children.first.children.first
|
|
63
62
|
end
|
|
64
63
|
end
|
|
65
64
|
end
|
data/lib/henitai/operators.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Henitai
|
|
|
16
16
|
autoload :ReturnValue, "henitai/operators/return_value"
|
|
17
17
|
autoload :ArrayDeclaration, "henitai/operators/array_declaration"
|
|
18
18
|
autoload :HashLiteral, "henitai/operators/hash_literal"
|
|
19
|
+
autoload :HashKeyType, "henitai/operators/hash_key_type"
|
|
19
20
|
autoload :RangeLiteral, "henitai/operators/range_literal"
|
|
20
21
|
autoload :SafeNavigation, "henitai/operators/safe_navigation"
|
|
21
22
|
autoload :PatternMatch, "henitai/operators/pattern_match"
|
|
@@ -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
|
@@ -353,6 +353,8 @@ module Henitai
|
|
|
353
353
|
class Operator
|
|
354
354
|
LIGHT_SET: Array[String]
|
|
355
355
|
FULL_SET: Array[String]
|
|
356
|
+
HARD_SET: Array[String]
|
|
357
|
+
SETS: Hash[Symbol, Array[String]]
|
|
356
358
|
|
|
357
359
|
def self.for_set: (Symbol) -> Array[untyped]
|
|
358
360
|
def self.node_types: () -> Array[Symbol]
|
|
@@ -718,6 +720,7 @@ module Henitai
|
|
|
718
720
|
def initialize: (reports_dir: String, ?coverage_report_reader: CoverageReportReader) -> void
|
|
719
721
|
def available?: () -> bool
|
|
720
722
|
def tests_covering: (untyped) -> Array[String]
|
|
723
|
+
def source_files_covered_by: (untyped) -> Array[String]
|
|
721
724
|
def covers?: (String, untyped) -> bool
|
|
722
725
|
|
|
723
726
|
private
|
|
@@ -852,6 +855,7 @@ module Henitai
|
|
|
852
855
|
def reused_verdicts_line: (Result) -> String?
|
|
853
856
|
def reused_mutants: (Result) -> Array[Mutant]
|
|
854
857
|
def executed_only_score_line: (Result) -> String?
|
|
858
|
+
def empty_since_scope_line: (Result) -> String?
|
|
855
859
|
def count_status: (Result, Symbol) -> Integer
|
|
856
860
|
def format_duration: (Float) -> String
|
|
857
861
|
def format_percent: (Float?) -> String
|
|
@@ -977,8 +981,9 @@ module Henitai
|
|
|
977
981
|
attr_reader survivor_stats: Hash[Symbol, untyped]?
|
|
978
982
|
attr_reader session_id: String
|
|
979
983
|
attr_reader git_sha: String?
|
|
984
|
+
attr_reader since: String?
|
|
980
985
|
|
|
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
|
|
986
|
+
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
987
|
def partial_rerun?: () -> bool
|
|
983
988
|
def authoritative?: () -> bool
|
|
984
989
|
def killed: () -> Integer
|
|
@@ -1086,6 +1091,8 @@ module Henitai
|
|
|
1086
1091
|
def reject_excluded: (Array[String]) -> Array[String]
|
|
1087
1092
|
def excluded_source_files: () -> Array[String]
|
|
1088
1093
|
def filter_changed: (Array[String]) -> Array[String]
|
|
1094
|
+
def changed_paths_since: () -> Array[String]
|
|
1095
|
+
def covered_sources_for_changed_tests: (Array[String]) -> Array[String]
|
|
1089
1096
|
def pattern_subjects: () -> Array[Subject]
|
|
1090
1097
|
def unique_subjects: (Array[Subject]) -> Array[Subject]
|
|
1091
1098
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Otten
|
|
@@ -166,6 +166,7 @@ files:
|
|
|
166
166
|
- lib/henitai/operators/conditional_expression.rb
|
|
167
167
|
- lib/henitai/operators/equality_identity_operator.rb
|
|
168
168
|
- lib/henitai/operators/equality_operator.rb
|
|
169
|
+
- lib/henitai/operators/hash_key_type.rb
|
|
169
170
|
- lib/henitai/operators/hash_literal.rb
|
|
170
171
|
- lib/henitai/operators/logical_operator.rb
|
|
171
172
|
- lib/henitai/operators/method_chain_unwrap.rb
|
|
@@ -223,7 +224,7 @@ metadata:
|
|
|
223
224
|
changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
|
|
224
225
|
documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
|
|
225
226
|
homepage_uri: https://github.com/martinotten/henitai
|
|
226
|
-
source_code_uri: https://github.com/martinotten/henitai/tree/v0.
|
|
227
|
+
source_code_uri: https://github.com/martinotten/henitai/tree/v0.4.0
|
|
227
228
|
rubygems_mfa_required: 'true'
|
|
228
229
|
rdoc_options: []
|
|
229
230
|
require_paths:
|