henitai 0.2.0 → 0.3.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 +105 -1
- data/README.md +138 -5
- data/assets/schema/henitai.schema.json +41 -0
- data/lib/henitai/available_cpu_count.rb +16 -23
- data/lib/henitai/canonical_report_merger.rb +107 -0
- data/lib/henitai/canonical_report_writer.rb +22 -0
- data/lib/henitai/checkpoint_reporter.rb +79 -0
- data/lib/henitai/cli/clean_command.rb +54 -0
- data/lib/henitai/cli/command_support.rb +52 -0
- data/lib/henitai/cli/init_command.rb +64 -0
- data/lib/henitai/cli/operator_command.rb +96 -0
- data/lib/henitai/cli/options.rb +84 -0
- data/lib/henitai/cli/run_command.rb +136 -0
- data/lib/henitai/cli/run_options.rb +108 -0
- data/lib/henitai/cli.rb +27 -407
- data/lib/henitai/composite_progress_reporter.rb +42 -0
- data/lib/henitai/configuration.rb +42 -9
- data/lib/henitai/configuration_validator/rules.rb +161 -0
- data/lib/henitai/configuration_validator/scalars.rb +169 -0
- data/lib/henitai/configuration_validator.rb +20 -240
- data/lib/henitai/coverage_bootstrapper.rb +54 -5
- data/lib/henitai/coverage_formatter.rb +5 -1
- data/lib/henitai/coverage_report_reader.rb +28 -5
- data/lib/henitai/eager_load.rb +36 -5
- data/lib/henitai/execution_engine/env_scope.rb +67 -0
- data/lib/henitai/execution_engine.rb +115 -53
- data/lib/henitai/generated_artifacts.rb +58 -0
- data/lib/henitai/incremental_filter.rb +100 -0
- data/lib/henitai/integration/base.rb +171 -0
- data/lib/henitai/integration/child_debug_support.rb +119 -0
- data/lib/henitai/integration/child_runtime_control.rb +50 -0
- data/lib/henitai/integration/coverage_suppression.rb +52 -0
- data/lib/henitai/integration/minitest.rb +101 -0
- data/lib/henitai/integration/minitest_load_path.rb +14 -0
- data/lib/henitai/integration/minitest_suite_command.rb +17 -0
- data/lib/henitai/integration/minitest_test_runner.rb +39 -0
- data/lib/henitai/integration/mutant_run_support.rb +77 -0
- data/lib/henitai/integration/rails_environment_preloader.rb +14 -0
- data/lib/henitai/integration/rspec_child_runner.rb +61 -0
- data/lib/henitai/integration/rspec_test_selection.rb +138 -0
- data/lib/henitai/integration/scenario_log_support.rb +160 -0
- data/lib/henitai/integration.rb +22 -846
- data/lib/henitai/minitest_coverage_reporter.rb +4 -1
- data/lib/henitai/mutant/activator.rb +15 -79
- data/lib/henitai/mutant/parameter_source.rb +98 -0
- data/lib/henitai/mutant.rb +14 -6
- data/lib/henitai/mutant_history_store/sql.rb +90 -0
- data/lib/henitai/mutant_history_store/verdict_cache.rb +84 -0
- data/lib/henitai/mutant_history_store.rb +67 -81
- data/lib/henitai/mutant_identity.rb +33 -2
- data/lib/henitai/mutation_skip_directives.rb +227 -0
- data/lib/henitai/operator.rb +3 -1
- data/lib/henitai/operators/equality_identity_operator.rb +51 -0
- data/lib/henitai/operators/equality_operator.rb +8 -3
- data/lib/henitai/operators.rb +1 -0
- data/lib/henitai/per_test_coverage.rb +66 -0
- data/lib/henitai/per_test_coverage_collector.rb +23 -6
- data/lib/henitai/per_test_coverage_selector.rb +11 -33
- data/lib/henitai/process_worker_runner.rb +48 -334
- data/lib/henitai/reporter/dashboard_metadata_provider.rb +113 -0
- data/lib/henitai/reporter.rb +258 -125
- data/lib/henitai/reports_directory_lock.rb +76 -0
- data/lib/henitai/result.rb +88 -23
- data/lib/henitai/runner.rb +137 -218
- data/lib/henitai/scenario_execution_result.rb +12 -0
- data/lib/henitai/slot_scheduler/draining.rb +146 -0
- data/lib/henitai/slot_scheduler/process_control.rb +43 -0
- data/lib/henitai/slot_scheduler.rb +272 -0
- data/lib/henitai/static_filter.rb +16 -6
- data/lib/henitai/survivor_rerun_strategy.rb +195 -0
- data/lib/henitai/survivor_test_filter.rb +1 -1
- data/lib/henitai/test_prioritizer.rb +28 -2
- data/lib/henitai/timeout_calibrator.rb +44 -0
- data/lib/henitai/unparse_helper.rb +5 -2
- data/lib/henitai/verdict_fingerprint.rb +155 -0
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +16 -1
- data/sig/configuration_validator.rbs +46 -22
- data/sig/henitai.rbs +386 -96
- metadata +45 -3
- data/lib/henitai/parallel_execution_runner.rb +0 -153
|
@@ -6,7 +6,6 @@ require "unparser"
|
|
|
6
6
|
module Henitai
|
|
7
7
|
class Mutant
|
|
8
8
|
# Activates a mutant inside the forked child process.
|
|
9
|
-
# rubocop:disable Metrics/ClassLength
|
|
10
9
|
class Activator
|
|
11
10
|
# Filters "already initialized constant" C-level warnings that fire when
|
|
12
11
|
# a source file is loaded into a process that already has the constant
|
|
@@ -24,17 +23,6 @@ module Henitai
|
|
|
24
23
|
end
|
|
25
24
|
Warning.singleton_class.prepend(ConstantRedefinitionFilter)
|
|
26
25
|
|
|
27
|
-
SERIALIZER_METHODS = {
|
|
28
|
-
arg: :argument_parameter_fragment,
|
|
29
|
-
optarg: :optional_parameter_fragment,
|
|
30
|
-
restarg: :rest_parameter_fragment,
|
|
31
|
-
kwarg: :keyword_parameter_fragment,
|
|
32
|
-
kwoptarg: :optional_keyword_parameter_fragment,
|
|
33
|
-
kwrestarg: :keyword_rest_parameter_fragment,
|
|
34
|
-
blockarg: :block_parameter_fragment,
|
|
35
|
-
forward_arg: :forward_parameter_fragment
|
|
36
|
-
}.freeze
|
|
37
|
-
|
|
38
26
|
def self.activate!(mutant)
|
|
39
27
|
new.activate!(mutant)
|
|
40
28
|
end
|
|
@@ -69,15 +57,27 @@ module Henitai
|
|
|
69
57
|
subject.method_type == :class ? target.singleton_class : target
|
|
70
58
|
end
|
|
71
59
|
|
|
60
|
+
# The trailing module_function re-copy handles module_function subjects:
|
|
61
|
+
# module_function copies the (private) instance method onto the module's
|
|
62
|
+
# singleton, and define_method only replaces the instance side — without
|
|
63
|
+
# the re-copy, callers of Module.method keep running the original code
|
|
64
|
+
# and every mutant in such modules falsely survives. The guard checks
|
|
65
|
+
# the pre-injection shape (private instance + singleton method on a
|
|
66
|
+
# non-Class module) so a class-level method that merely shares an
|
|
67
|
+
# instance method's name is never clobbered.
|
|
72
68
|
def method_source(mutant)
|
|
73
69
|
method_name = mutant.subject.method_name
|
|
74
70
|
parameters = parameter_source(mutant)
|
|
75
71
|
replacement = body_source(mutant)
|
|
76
72
|
|
|
77
73
|
<<~RUBY
|
|
74
|
+
__henitai_module_function__ = !is_a?(Class) &&
|
|
75
|
+
private_method_defined?(:#{method_name}) &&
|
|
76
|
+
singleton_class.method_defined?(:#{method_name})
|
|
78
77
|
define_method(:#{method_name}) do |#{parameters}|
|
|
79
78
|
#{replacement}
|
|
80
79
|
end
|
|
80
|
+
module_function :#{method_name} if __henitai_module_function__
|
|
81
81
|
RUBY
|
|
82
82
|
end
|
|
83
83
|
|
|
@@ -124,78 +124,13 @@ module Henitai
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
def parameter_source(mutant)
|
|
127
|
-
|
|
128
|
-
return "" unless args_node
|
|
129
|
-
return forward_parameter_fragment(nil) if args_node.type == :forward_args
|
|
130
|
-
|
|
131
|
-
args_node.children.filter_map do |argument|
|
|
132
|
-
parameter_fragment(argument)
|
|
133
|
-
end.join(", ")
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def method_arguments(subject_node)
|
|
137
|
-
case subject_node&.type
|
|
138
|
-
when :def
|
|
139
|
-
subject_node.children[1]
|
|
140
|
-
when :defs
|
|
141
|
-
subject_node.children[2]
|
|
142
|
-
when :block
|
|
143
|
-
block_arguments(subject_node)
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def parameter_fragment(argument)
|
|
148
|
-
method_name = SERIALIZER_METHODS[argument&.type]
|
|
149
|
-
return unless method_name
|
|
150
|
-
|
|
151
|
-
send(method_name, argument)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def argument_parameter_fragment(argument)
|
|
155
|
-
argument.children[0].to_s
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def optional_parameter_fragment(argument)
|
|
159
|
-
"#{argument.children[0]} = #{compile_safe_unparse(argument.children[1])}"
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def rest_parameter_fragment(argument)
|
|
163
|
-
prefixed_parameter(argument, "*")
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def keyword_parameter_fragment(argument)
|
|
167
|
-
"#{argument.children[0]}:"
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def optional_keyword_parameter_fragment(argument)
|
|
171
|
-
"#{argument.children[0]}: #{compile_safe_unparse(argument.children[1])}"
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def keyword_rest_parameter_fragment(argument)
|
|
175
|
-
prefixed_parameter(argument, "**")
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def block_parameter_fragment(argument)
|
|
179
|
-
"&#{argument.children[0]}"
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def forward_parameter_fragment(_argument)
|
|
183
|
-
"*args, **kwargs, &block"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def prefixed_parameter(argument, prefix)
|
|
187
|
-
name = argument.children[0]
|
|
188
|
-
name ? "#{prefix}#{name}" : prefix
|
|
127
|
+
ParameterSource.new.build(mutant.subject.ast_node)
|
|
189
128
|
end
|
|
190
129
|
|
|
191
130
|
def block_body(subject_node)
|
|
192
131
|
subject_node.children[2]
|
|
193
132
|
end
|
|
194
133
|
|
|
195
|
-
def block_arguments(subject_node)
|
|
196
|
-
subject_node.children[1]
|
|
197
|
-
end
|
|
198
|
-
|
|
199
134
|
def heredoc_location?(location)
|
|
200
135
|
location.respond_to?(:heredoc_body) && location.heredoc_body
|
|
201
136
|
end
|
|
@@ -241,6 +176,8 @@ module Henitai
|
|
|
241
176
|
|
|
242
177
|
Thread.current[:henitai_filter_const_warnings] = true
|
|
243
178
|
load(source_file)
|
|
179
|
+
loaded_feature = File.expand_path(source_file)
|
|
180
|
+
$LOADED_FEATURES << loaded_feature unless $LOADED_FEATURES.include?(loaded_feature)
|
|
244
181
|
ensure
|
|
245
182
|
Thread.current[:henitai_filter_const_warnings] = false
|
|
246
183
|
end
|
|
@@ -277,6 +214,5 @@ module Henitai
|
|
|
277
214
|
raise Unparser::UnsupportedNodeError, e.message
|
|
278
215
|
end
|
|
279
216
|
end
|
|
280
|
-
# rubocop:enable Metrics/ClassLength
|
|
281
217
|
end
|
|
282
218
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
require "unparser"
|
|
5
|
+
|
|
6
|
+
module Henitai
|
|
7
|
+
class Mutant
|
|
8
|
+
# Builds the parameter-list fragment of a +define_method+ block from a
|
|
9
|
+
# subject's method-definition AST node.
|
|
10
|
+
#
|
|
11
|
+
# Given the +def+/+defs+/+block+ node for a method, {#build} returns the
|
|
12
|
+
# comma-separated parameter source (e.g. "a, b = 1, *rest, key:, &blk")
|
|
13
|
+
# suitable for splicing into a +define_method(:name) do |...| ...+ template.
|
|
14
|
+
class ParameterSource
|
|
15
|
+
SERIALIZER_METHODS = {
|
|
16
|
+
arg: :argument_parameter_fragment,
|
|
17
|
+
optarg: :optional_parameter_fragment,
|
|
18
|
+
restarg: :rest_parameter_fragment,
|
|
19
|
+
kwarg: :keyword_parameter_fragment,
|
|
20
|
+
kwoptarg: :optional_keyword_parameter_fragment,
|
|
21
|
+
kwrestarg: :keyword_rest_parameter_fragment,
|
|
22
|
+
blockarg: :block_parameter_fragment,
|
|
23
|
+
forward_arg: :forward_parameter_fragment
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def build(subject_node)
|
|
27
|
+
args_node = method_arguments(subject_node)
|
|
28
|
+
return "" unless args_node
|
|
29
|
+
return forward_parameter_fragment(nil) if args_node.type == :forward_args
|
|
30
|
+
|
|
31
|
+
args_node.children.filter_map do |argument|
|
|
32
|
+
parameter_fragment(argument)
|
|
33
|
+
end.join(", ")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def method_arguments(subject_node)
|
|
39
|
+
case subject_node&.type
|
|
40
|
+
when :def, :block
|
|
41
|
+
subject_node.children[1]
|
|
42
|
+
when :defs
|
|
43
|
+
subject_node.children[2]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parameter_fragment(argument)
|
|
48
|
+
method_name = SERIALIZER_METHODS[argument&.type]
|
|
49
|
+
return unless method_name
|
|
50
|
+
|
|
51
|
+
send(method_name, argument)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def argument_parameter_fragment(argument)
|
|
55
|
+
argument.children[0].to_s
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def optional_parameter_fragment(argument)
|
|
59
|
+
"#{argument.children[0]} = #{compile_safe_unparse(argument.children[1])}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def rest_parameter_fragment(argument)
|
|
63
|
+
prefixed_parameter(argument, "*")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def keyword_parameter_fragment(argument)
|
|
67
|
+
"#{argument.children[0]}:"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def optional_keyword_parameter_fragment(argument)
|
|
71
|
+
"#{argument.children[0]}: #{compile_safe_unparse(argument.children[1])}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def keyword_rest_parameter_fragment(argument)
|
|
75
|
+
prefixed_parameter(argument, "**")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def block_parameter_fragment(argument)
|
|
79
|
+
"&#{argument.children[0]}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def forward_parameter_fragment(_argument)
|
|
83
|
+
"*args, **kwargs, &block"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def prefixed_parameter(argument, prefix)
|
|
87
|
+
name = argument.children[0]
|
|
88
|
+
name ? "#{prefix}#{name}" : prefix
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def compile_safe_unparse(node)
|
|
92
|
+
Unparser.unparse(node)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
raise Unparser::UnsupportedNodeError, e.message
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/henitai/mutant.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require_relative "mutant_identity"
|
|
5
|
+
require_relative "unparse_helper"
|
|
5
6
|
|
|
6
7
|
module Henitai
|
|
7
8
|
# Represents a single syntactic mutation applied to a Subject.
|
|
@@ -16,7 +17,10 @@ module Henitai
|
|
|
16
17
|
# :pending, :killed, :survived, :timeout, :compile_error, :runtime_error,
|
|
17
18
|
# :ignored, :no_coverage
|
|
18
19
|
class Mutant
|
|
20
|
+
include UnparseHelper
|
|
21
|
+
|
|
19
22
|
autoload :Activator, "henitai/mutant/activator"
|
|
23
|
+
autoload :ParameterSource, "henitai/mutant/parameter_source"
|
|
20
24
|
|
|
21
25
|
# Status-Vokabular folgt dem Stryker mutation-testing-report-schema.
|
|
22
26
|
# :equivalent ist ein Henitai-interner Status (wird im JSON als "Ignored" serialisiert,
|
|
@@ -37,7 +41,8 @@ module Henitai
|
|
|
37
41
|
attr_reader :id, :subject, :operator, :original_node, :mutated_node,
|
|
38
42
|
:mutation_type, :description, :location,
|
|
39
43
|
:precomputed_stable_id, :precomputed_activation_source
|
|
40
|
-
attr_accessor :status, :killing_test, :duration, :covered_by, :tests_completed
|
|
44
|
+
attr_accessor :status, :killing_test, :duration, :covered_by, :tests_completed,
|
|
45
|
+
:ignore_reason, :from_cache
|
|
41
46
|
|
|
42
47
|
# @param subject [Subject] the subject being mutated
|
|
43
48
|
# @param operator [Symbol] operator name, e.g. :ArithmeticOperator
|
|
@@ -56,11 +61,9 @@ module Henitai
|
|
|
56
61
|
@location = location
|
|
57
62
|
@precomputed_stable_id = precomputed_stable_id
|
|
58
63
|
@precomputed_activation_source = precomputed_activation_source
|
|
59
|
-
@status
|
|
60
|
-
@killing_test
|
|
61
|
-
@
|
|
62
|
-
@covered_by = nil
|
|
63
|
-
@tests_completed = nil
|
|
64
|
+
@status = :pending
|
|
65
|
+
@killing_test = @duration = @covered_by = @tests_completed = @ignore_reason = nil
|
|
66
|
+
@from_cache = false
|
|
64
67
|
end
|
|
65
68
|
# rubocop:enable Metrics/ParameterLists
|
|
66
69
|
|
|
@@ -68,7 +71,12 @@ module Henitai
|
|
|
68
71
|
@stable_id ||= @precomputed_stable_id || MutantIdentity.stable_id(self)
|
|
69
72
|
end
|
|
70
73
|
|
|
74
|
+
# Unparsing is expensive and the value is needed by both the JSON and
|
|
75
|
+
# the terminal reporter — compute once per mutant.
|
|
76
|
+
def mutated_source = @mutated_source ||= safe_unparse(mutated_node)
|
|
77
|
+
|
|
71
78
|
def killed? = @status == :killed
|
|
79
|
+
def from_cache? = @from_cache == true
|
|
72
80
|
def survived? = @status == :survived
|
|
73
81
|
def pending? = @status == :pending
|
|
74
82
|
def ignored? = @status == :ignored
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
class MutantHistoryStore
|
|
5
|
+
# SQL statements used by {MutantHistoryStore} to create the schema and
|
|
6
|
+
# persist run/mutant rows in the SQLite history database.
|
|
7
|
+
module Sql
|
|
8
|
+
RUNS_TABLE = <<~SQL
|
|
9
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
version TEXT NOT NULL,
|
|
12
|
+
recorded_at TEXT NOT NULL,
|
|
13
|
+
mutation_score REAL,
|
|
14
|
+
mutation_score_indicator REAL,
|
|
15
|
+
equivalence_uncertainty TEXT,
|
|
16
|
+
total_mutants INTEGER NOT NULL,
|
|
17
|
+
killed_mutants INTEGER NOT NULL,
|
|
18
|
+
survived_mutants INTEGER NOT NULL,
|
|
19
|
+
timeout_mutants INTEGER NOT NULL,
|
|
20
|
+
equivalent_mutants INTEGER NOT NULL
|
|
21
|
+
);
|
|
22
|
+
SQL
|
|
23
|
+
|
|
24
|
+
MUTANTS_TABLE = <<~SQL
|
|
25
|
+
CREATE TABLE IF NOT EXISTS mutants (
|
|
26
|
+
mutant_id TEXT PRIMARY KEY,
|
|
27
|
+
first_seen_version TEXT NOT NULL,
|
|
28
|
+
first_seen_at TEXT NOT NULL,
|
|
29
|
+
last_seen_version TEXT NOT NULL,
|
|
30
|
+
last_seen_at TEXT NOT NULL,
|
|
31
|
+
current_status TEXT NOT NULL,
|
|
32
|
+
status_history TEXT NOT NULL,
|
|
33
|
+
days_alive INTEGER NOT NULL
|
|
34
|
+
);
|
|
35
|
+
SQL
|
|
36
|
+
|
|
37
|
+
INSERT_RUN = <<~SQL
|
|
38
|
+
INSERT INTO runs (
|
|
39
|
+
version,
|
|
40
|
+
recorded_at,
|
|
41
|
+
mutation_score,
|
|
42
|
+
mutation_score_indicator,
|
|
43
|
+
equivalence_uncertainty,
|
|
44
|
+
total_mutants,
|
|
45
|
+
killed_mutants,
|
|
46
|
+
survived_mutants,
|
|
47
|
+
timeout_mutants,
|
|
48
|
+
equivalent_mutants
|
|
49
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
50
|
+
SQL
|
|
51
|
+
|
|
52
|
+
# Nullable verdict-cache columns added after the initial release; see
|
|
53
|
+
# MIGRATION_COLUMNS. Old rows keep NULL there, which the incremental
|
|
54
|
+
# filter treats as never-reusable — the correct conservative behavior.
|
|
55
|
+
MIGRATION_COLUMNS = {
|
|
56
|
+
"subject_source_hash" => "TEXT",
|
|
57
|
+
"covered_tests_fingerprint" => "TEXT"
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
UPSERT_MUTANT = <<~SQL
|
|
61
|
+
INSERT INTO mutants (
|
|
62
|
+
mutant_id,
|
|
63
|
+
first_seen_version,
|
|
64
|
+
first_seen_at,
|
|
65
|
+
last_seen_version,
|
|
66
|
+
last_seen_at,
|
|
67
|
+
current_status,
|
|
68
|
+
status_history,
|
|
69
|
+
days_alive,
|
|
70
|
+
subject_source_hash,
|
|
71
|
+
covered_tests_fingerprint
|
|
72
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
73
|
+
ON CONFLICT(mutant_id) DO UPDATE SET
|
|
74
|
+
last_seen_version = excluded.last_seen_version,
|
|
75
|
+
last_seen_at = excluded.last_seen_at,
|
|
76
|
+
current_status = excluded.current_status,
|
|
77
|
+
status_history = excluded.status_history,
|
|
78
|
+
days_alive = excluded.days_alive,
|
|
79
|
+
subject_source_hash = excluded.subject_source_hash,
|
|
80
|
+
covered_tests_fingerprint = excluded.covered_tests_fingerprint
|
|
81
|
+
SQL
|
|
82
|
+
|
|
83
|
+
VERDICT_LOOKUP = <<~SQL
|
|
84
|
+
SELECT current_status, subject_source_hash, covered_tests_fingerprint
|
|
85
|
+
FROM mutants
|
|
86
|
+
WHERE mutant_id = ?
|
|
87
|
+
SQL
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../verdict_fingerprint"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
class MutantHistoryStore
|
|
7
|
+
# Verdict-cache column handling for the incremental (`--incremental`)
|
|
8
|
+
# reuse feature. Mixed into {MutantHistoryStore}.
|
|
9
|
+
module VerdictCache
|
|
10
|
+
# The reusable verdicts: hashes are persisted for killed and survived
|
|
11
|
+
# mutants only; every other status stays NULL and always re-executes.
|
|
12
|
+
CACHEABLE_STATUSES = %w[killed survived].freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# Cache-hit mutants (from_cache) were never executed this run, so their
|
|
17
|
+
# covered tests are unknown — carry the stored fingerprints forward
|
|
18
|
+
# instead of recomputing them from empty data (which would wipe them
|
|
19
|
+
# and silently disable reuse after one cached run).
|
|
20
|
+
def verdict_cache_bindings(mutant, existing_row)
|
|
21
|
+
status = mutant.status.to_s
|
|
22
|
+
return [nil, nil] unless CACHEABLE_STATUSES.include?(status)
|
|
23
|
+
return existing_cache_bindings(existing_row) if from_cache?(mutant)
|
|
24
|
+
return killed_cache_bindings(mutant) if status == "killed"
|
|
25
|
+
|
|
26
|
+
survived_cache_bindings(mutant, existing_row)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def killed_cache_bindings(mutant)
|
|
30
|
+
[
|
|
31
|
+
VerdictFingerprint.subject_source_hash(mutant),
|
|
32
|
+
VerdictFingerprint.tests_fingerprint(covered_tests_for(mutant))
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Survived rows fingerprint the full-map intersection set — the same
|
|
37
|
+
# set the incremental filter recomputes live — never Mutant#covered_by,
|
|
38
|
+
# which reflects selector fallback and test_excludes and would
|
|
39
|
+
# permanently mismatch the live set. When the fingerprint cannot be
|
|
40
|
+
# recomputed (no per-test map, recipe stubs without a source range),
|
|
41
|
+
# the previously stored bindings are kept: the filter re-validates
|
|
42
|
+
# everything live before reuse, so a carried-forward fingerprint can
|
|
43
|
+
# never validate a changed state.
|
|
44
|
+
def survived_cache_bindings(mutant, existing_row)
|
|
45
|
+
bindings = [
|
|
46
|
+
VerdictFingerprint.subject_source_hash(mutant),
|
|
47
|
+
survived_tests_fingerprint(mutant)
|
|
48
|
+
]
|
|
49
|
+
return bindings unless bindings.any?(&:nil?)
|
|
50
|
+
|
|
51
|
+
existing_cache_bindings(existing_row)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def survived_tests_fingerprint(mutant)
|
|
55
|
+
return nil unless per_test_coverage
|
|
56
|
+
|
|
57
|
+
VerdictFingerprint.survivor_tests_fingerprint(
|
|
58
|
+
per_test_coverage.tests_covering(mutant),
|
|
59
|
+
dependency_sha: recording_dependency_sha
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# One dependency sha per record call set, computed lazily so runs
|
|
64
|
+
# without survivors never touch the dependency files.
|
|
65
|
+
def recording_dependency_sha
|
|
66
|
+
@recording_dependency_sha ||= VerdictFingerprint.dependency_fingerprint
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def existing_cache_bindings(existing_row)
|
|
70
|
+
return [nil, nil] unless existing_row
|
|
71
|
+
|
|
72
|
+
[existing_row["subject_source_hash"], existing_row["covered_tests_fingerprint"]]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def from_cache?(mutant)
|
|
76
|
+
mutant.respond_to?(:from_cache?) && mutant.from_cache?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def covered_tests_for(mutant)
|
|
80
|
+
mutant.respond_to?(:covered_by) ? mutant.covered_by : nil
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -6,76 +6,20 @@ require "json"
|
|
|
6
6
|
require "sqlite3"
|
|
7
7
|
require "time"
|
|
8
8
|
require_relative "mutant_identity"
|
|
9
|
+
require_relative "mutant_history_store/sql"
|
|
10
|
+
require_relative "mutant_history_store/verdict_cache"
|
|
9
11
|
|
|
10
12
|
module Henitai
|
|
11
13
|
# Persists mutant outcomes across runs in a lightweight SQLite database.
|
|
12
|
-
# rubocop:disable Metrics/ClassLength
|
|
13
14
|
class MutantHistoryStore
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
mutation_score_indicator REAL,
|
|
21
|
-
equivalence_uncertainty TEXT,
|
|
22
|
-
total_mutants INTEGER NOT NULL,
|
|
23
|
-
killed_mutants INTEGER NOT NULL,
|
|
24
|
-
survived_mutants INTEGER NOT NULL,
|
|
25
|
-
timeout_mutants INTEGER NOT NULL,
|
|
26
|
-
equivalent_mutants INTEGER NOT NULL
|
|
27
|
-
);
|
|
28
|
-
SQL
|
|
29
|
-
|
|
30
|
-
MUTANTS_TABLE_SQL = <<~SQL
|
|
31
|
-
CREATE TABLE IF NOT EXISTS mutants (
|
|
32
|
-
mutant_id TEXT PRIMARY KEY,
|
|
33
|
-
first_seen_version TEXT NOT NULL,
|
|
34
|
-
first_seen_at TEXT NOT NULL,
|
|
35
|
-
last_seen_version TEXT NOT NULL,
|
|
36
|
-
last_seen_at TEXT NOT NULL,
|
|
37
|
-
current_status TEXT NOT NULL,
|
|
38
|
-
status_history TEXT NOT NULL,
|
|
39
|
-
days_alive INTEGER NOT NULL
|
|
40
|
-
);
|
|
41
|
-
SQL
|
|
42
|
-
|
|
43
|
-
INSERT_RUN_SQL = <<~SQL
|
|
44
|
-
INSERT INTO runs (
|
|
45
|
-
version,
|
|
46
|
-
recorded_at,
|
|
47
|
-
mutation_score,
|
|
48
|
-
mutation_score_indicator,
|
|
49
|
-
equivalence_uncertainty,
|
|
50
|
-
total_mutants,
|
|
51
|
-
killed_mutants,
|
|
52
|
-
survived_mutants,
|
|
53
|
-
timeout_mutants,
|
|
54
|
-
equivalent_mutants
|
|
55
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
56
|
-
SQL
|
|
57
|
-
|
|
58
|
-
UPSERT_MUTANT_SQL = <<~SQL
|
|
59
|
-
INSERT INTO mutants (
|
|
60
|
-
mutant_id,
|
|
61
|
-
first_seen_version,
|
|
62
|
-
first_seen_at,
|
|
63
|
-
last_seen_version,
|
|
64
|
-
last_seen_at,
|
|
65
|
-
current_status,
|
|
66
|
-
status_history,
|
|
67
|
-
days_alive
|
|
68
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
69
|
-
ON CONFLICT(mutant_id) DO UPDATE SET
|
|
70
|
-
last_seen_version = excluded.last_seen_version,
|
|
71
|
-
last_seen_at = excluded.last_seen_at,
|
|
72
|
-
current_status = excluded.current_status,
|
|
73
|
-
status_history = excluded.status_history,
|
|
74
|
-
days_alive = excluded.days_alive
|
|
75
|
-
SQL
|
|
76
|
-
|
|
77
|
-
def initialize(path:)
|
|
15
|
+
include VerdictCache
|
|
16
|
+
|
|
17
|
+
# @param per_test_coverage [PerTestCoverage, nil] live per-test coverage
|
|
18
|
+
# view used to record the full-map intersection set for survived
|
|
19
|
+
# verdicts; without it survived rows stay NULL (never reusable).
|
|
20
|
+
def initialize(path:, per_test_coverage: nil)
|
|
78
21
|
@path = path
|
|
22
|
+
@per_test_coverage = per_test_coverage
|
|
79
23
|
end
|
|
80
24
|
|
|
81
25
|
attr_reader :path
|
|
@@ -94,6 +38,27 @@ module Henitai
|
|
|
94
38
|
end
|
|
95
39
|
end
|
|
96
40
|
|
|
41
|
+
# Verdict-cache lookup for `--incremental`: returns the stored hashes for
|
|
42
|
+
# the mutant's LATEST verdict when it is Killed or Survived — the upsert
|
|
43
|
+
# keeps exactly one row per stable id, so current_status is always the
|
|
44
|
+
# most recent outcome. nil for every other status, unknown ids and legacy
|
|
45
|
+
# rows without hashes (recorded before the cache columns existed).
|
|
46
|
+
def verdict_for(stable_id)
|
|
47
|
+
return nil unless File.exist?(path)
|
|
48
|
+
|
|
49
|
+
with_database do |db|
|
|
50
|
+
ensure_schema(db)
|
|
51
|
+
verdict_from_row(db.get_first_row(Sql::VERDICT_LOOKUP, stable_id))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Killed-only view of {#verdict_for}, kept for the original killed reuse
|
|
56
|
+
# path.
|
|
57
|
+
def killed_verdict_for(stable_id)
|
|
58
|
+
verdict = verdict_for(stable_id)
|
|
59
|
+
verdict if verdict && verdict[:status] == :killed
|
|
60
|
+
end
|
|
61
|
+
|
|
97
62
|
def trend_report
|
|
98
63
|
with_database do |db|
|
|
99
64
|
ensure_schema(db)
|
|
@@ -107,6 +72,19 @@ module Henitai
|
|
|
107
72
|
|
|
108
73
|
private
|
|
109
74
|
|
|
75
|
+
attr_reader :per_test_coverage
|
|
76
|
+
|
|
77
|
+
def verdict_from_row(row)
|
|
78
|
+
return nil unless row && VerdictCache::CACHEABLE_STATUSES.include?(row["current_status"])
|
|
79
|
+
return nil if row["subject_source_hash"].nil? || row["covered_tests_fingerprint"].nil?
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
status: row["current_status"].to_sym,
|
|
83
|
+
subject_source_hash: row["subject_source_hash"],
|
|
84
|
+
covered_tests_fingerprint: row["covered_tests_fingerprint"]
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
110
88
|
def partial_rerun?(result)
|
|
111
89
|
result.respond_to?(:partial_rerun?) && result.partial_rerun?
|
|
112
90
|
end
|
|
@@ -120,17 +98,30 @@ module Henitai
|
|
|
120
98
|
end
|
|
121
99
|
|
|
122
100
|
def ensure_schema(db)
|
|
123
|
-
db.execute_batch(
|
|
124
|
-
db.execute_batch(
|
|
101
|
+
db.execute_batch(Sql::RUNS_TABLE)
|
|
102
|
+
db.execute_batch(Sql::MUTANTS_TABLE)
|
|
103
|
+
migrate_mutants_table(db)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Additive, idempotent in-place migration: databases created before the
|
|
107
|
+
# verdict-cache columns existed gain them on first contact, with existing
|
|
108
|
+
# rows left intact (NULL hashes — valid but never reusable).
|
|
109
|
+
def migrate_mutants_table(db)
|
|
110
|
+
existing = db.execute("PRAGMA table_info(mutants)").map { |row| row["name"] }
|
|
111
|
+
Sql::MIGRATION_COLUMNS.each do |column, type|
|
|
112
|
+
next if existing.include?(column)
|
|
113
|
+
|
|
114
|
+
db.execute("ALTER TABLE mutants ADD COLUMN #{column} #{type}")
|
|
115
|
+
end
|
|
125
116
|
end
|
|
126
117
|
|
|
127
118
|
def insert_run(db, result, version, recorded_at)
|
|
128
|
-
db.execute(
|
|
119
|
+
db.execute(Sql::INSERT_RUN, insert_run_bindings(result, version, recorded_at))
|
|
129
120
|
end
|
|
130
121
|
|
|
131
122
|
def upsert_mutant(db, mutant, version, recorded_at)
|
|
132
123
|
db.execute(
|
|
133
|
-
|
|
124
|
+
Sql::UPSERT_MUTANT,
|
|
134
125
|
upsert_mutant_bindings(mutant_history_data(db, mutant, version, recorded_at))
|
|
135
126
|
)
|
|
136
127
|
end
|
|
@@ -157,18 +148,12 @@ module Henitai
|
|
|
157
148
|
def mutant_history_data(db, mutant, version, recorded_at)
|
|
158
149
|
mutant_id = stable_mutant_id(mutant)
|
|
159
150
|
existing = existing_mutant_row(db, mutant_id)
|
|
160
|
-
history = existing_status_history(existing)
|
|
161
|
-
history << mutation_history_entry(mutant, version, recorded_at)
|
|
151
|
+
history = existing_status_history(existing) << mutation_history_entry(mutant, version, recorded_at)
|
|
162
152
|
first_seen = first_seen_metadata(existing, version, recorded_at)
|
|
163
153
|
|
|
164
154
|
{
|
|
165
|
-
mutant_id:
|
|
166
|
-
first_seen_version: first_seen[:version],
|
|
167
|
-
first_seen_at: first_seen[:at],
|
|
168
|
-
version: version,
|
|
169
|
-
recorded_at: recorded_at,
|
|
170
|
-
mutant: mutant,
|
|
171
|
-
history: history,
|
|
155
|
+
mutant_id:, version:, recorded_at:, mutant:, history:, existing_row: existing,
|
|
156
|
+
first_seen_version: first_seen[:version], first_seen_at: first_seen[:at],
|
|
172
157
|
days_alive: days_alive_since(first_seen[:at], recorded_at)
|
|
173
158
|
}
|
|
174
159
|
end
|
|
@@ -248,17 +233,18 @@ module Henitai
|
|
|
248
233
|
end
|
|
249
234
|
|
|
250
235
|
def upsert_mutant_bindings(data)
|
|
236
|
+
mutant = data.fetch(:mutant)
|
|
251
237
|
[
|
|
252
238
|
data.fetch(:mutant_id),
|
|
253
239
|
data.fetch(:first_seen_version),
|
|
254
240
|
data.fetch(:first_seen_at),
|
|
255
241
|
data.fetch(:version),
|
|
256
242
|
data.fetch(:recorded_at).iso8601,
|
|
257
|
-
|
|
243
|
+
mutant.status.to_s,
|
|
258
244
|
JSON.generate(data.fetch(:history)),
|
|
259
|
-
data.fetch(:days_alive)
|
|
245
|
+
data.fetch(:days_alive),
|
|
246
|
+
*verdict_cache_bindings(mutant, data[:existing_row])
|
|
260
247
|
]
|
|
261
248
|
end
|
|
262
249
|
end
|
|
263
250
|
end
|
|
264
|
-
# rubocop:enable Metrics/ClassLength
|