henitai 0.1.10 → 0.2.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 +94 -1
- data/README.md +33 -7
- data/assets/schema/henitai.schema.json +6 -0
- data/lib/henitai/cli/clean_command.rb +48 -0
- data/lib/henitai/cli/command_support.rb +51 -0
- data/lib/henitai/cli/init_command.rb +64 -0
- data/lib/henitai/cli/operator_command.rb +95 -0
- data/lib/henitai/cli/options.rb +120 -0
- data/lib/henitai/cli/run_command.rb +103 -0
- data/lib/henitai/cli.rb +17 -327
- data/lib/henitai/configuration.rb +26 -12
- data/lib/henitai/configuration_validator/rules.rb +143 -0
- data/lib/henitai/configuration_validator/scalars.rb +123 -0
- data/lib/henitai/configuration_validator.rb +12 -239
- data/lib/henitai/coverage_bootstrapper.rb +24 -24
- data/lib/henitai/eager_load.rb +36 -5
- data/lib/henitai/execution_engine.rb +6 -11
- data/lib/henitai/git_diff_analyzer.rb +34 -0
- data/lib/henitai/integration/base.rb +171 -0
- data/lib/henitai/integration/child_debug_support.rb +115 -0
- data/lib/henitai/integration/child_runtime_control.rb +50 -0
- data/lib/henitai/integration/coverage_suppression.rb +43 -0
- data/lib/henitai/integration/minitest.rb +133 -0
- data/lib/henitai/integration/mutant_run_support.rb +77 -0
- data/lib/henitai/integration/rspec_child_runner.rb +61 -0
- data/lib/henitai/integration/rspec_process_runner.rb +66 -13
- data/lib/henitai/integration/rspec_test_selection.rb +135 -0
- data/lib/henitai/integration/scenario_log_support.rb +116 -0
- data/lib/henitai/integration.rb +43 -519
- data/lib/henitai/mutant/activator.rb +13 -79
- data/lib/henitai/mutant/parameter_source.rb +98 -0
- data/lib/henitai/mutant.rb +14 -2
- data/lib/henitai/mutant_generator.rb +21 -2
- data/lib/henitai/mutant_history_store/sql.rb +72 -0
- data/lib/henitai/mutant_history_store.rb +12 -91
- data/lib/henitai/mutant_identity.rb +34 -0
- data/lib/henitai/parallel_execution_runner.rb +29 -11
- data/lib/henitai/per_test_coverage_collector.rb +3 -1
- data/lib/henitai/process_wakeup.rb +49 -0
- data/lib/henitai/process_worker_runner.rb +148 -0
- data/lib/henitai/reporter.rb +96 -11
- data/lib/henitai/result.rb +49 -16
- data/lib/henitai/runner.rb +96 -30
- data/lib/henitai/scenario_execution_result.rb +16 -3
- data/lib/henitai/slot_scheduler/draining.rb +140 -0
- data/lib/henitai/slot_scheduler/process_control.rb +43 -0
- data/lib/henitai/slot_scheduler.rb +214 -0
- data/lib/henitai/static_filter.rb +10 -3
- data/lib/henitai/survivor_activation_cache.rb +81 -0
- data/lib/henitai/survivor_loader.rb +140 -0
- data/lib/henitai/survivor_rerun_strategy.rb +195 -0
- data/lib/henitai/survivor_selector.rb +36 -0
- data/lib/henitai/survivor_test_filter.rb +72 -0
- data/lib/henitai/unparse_helper.rb +5 -2
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +10 -0
- data/sig/configuration_validator.rbs +46 -22
- data/sig/henitai.rbs +329 -53
- metadata +46 -2
|
@@ -23,31 +23,30 @@ module Henitai
|
|
|
23
23
|
end
|
|
24
24
|
Warning.singleton_class.prepend(ConstantRedefinitionFilter)
|
|
25
25
|
|
|
26
|
-
SERIALIZER_METHODS = {
|
|
27
|
-
arg: :argument_parameter_fragment,
|
|
28
|
-
optarg: :optional_parameter_fragment,
|
|
29
|
-
restarg: :rest_parameter_fragment,
|
|
30
|
-
kwarg: :keyword_parameter_fragment,
|
|
31
|
-
kwoptarg: :optional_keyword_parameter_fragment,
|
|
32
|
-
kwrestarg: :keyword_rest_parameter_fragment,
|
|
33
|
-
blockarg: :block_parameter_fragment,
|
|
34
|
-
forward_arg: :forward_parameter_fragment
|
|
35
|
-
}.freeze
|
|
36
|
-
|
|
37
26
|
def self.activate!(mutant)
|
|
38
27
|
new.activate!(mutant)
|
|
39
28
|
end
|
|
40
29
|
|
|
30
|
+
# Returns the +define_method+ source string for +mutant+ without
|
|
31
|
+
# actually evaluating it. Used to pre-compute activation recipes.
|
|
32
|
+
# Returns nil if the source cannot be computed (e.g. unsupported AST node).
|
|
33
|
+
def self.activation_source_for(mutant)
|
|
34
|
+
new.send(:method_source, mutant)
|
|
35
|
+
rescue StandardError
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
41
39
|
def activate!(mutant)
|
|
42
40
|
subject = mutant.subject
|
|
43
41
|
raise ArgumentError, "Cannot activate wildcard subjects" if subject.method_name.nil?
|
|
44
42
|
|
|
43
|
+
source = mutant.precomputed_activation_source || method_source(mutant)
|
|
45
44
|
target = target_for(subject)
|
|
46
45
|
Henitai::WarningSilencer.silence do
|
|
47
|
-
target.class_eval(
|
|
46
|
+
target.class_eval(source, __FILE__, __LINE__ + 1)
|
|
48
47
|
nil
|
|
49
48
|
end
|
|
50
|
-
rescue Unparser::UnsupportedNodeError
|
|
49
|
+
rescue Unparser::UnsupportedNodeError, SyntaxError
|
|
51
50
|
:compile_error
|
|
52
51
|
end
|
|
53
52
|
|
|
@@ -113,78 +112,13 @@ module Henitai
|
|
|
113
112
|
end
|
|
114
113
|
|
|
115
114
|
def parameter_source(mutant)
|
|
116
|
-
|
|
117
|
-
return "" unless args_node
|
|
118
|
-
return forward_parameter_fragment(nil) if args_node.type == :forward_args
|
|
119
|
-
|
|
120
|
-
args_node.children.filter_map do |argument|
|
|
121
|
-
parameter_fragment(argument)
|
|
122
|
-
end.join(", ")
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def method_arguments(subject_node)
|
|
126
|
-
case subject_node&.type
|
|
127
|
-
when :def
|
|
128
|
-
subject_node.children[1]
|
|
129
|
-
when :defs
|
|
130
|
-
subject_node.children[2]
|
|
131
|
-
when :block
|
|
132
|
-
block_arguments(subject_node)
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def parameter_fragment(argument)
|
|
137
|
-
method_name = SERIALIZER_METHODS[argument&.type]
|
|
138
|
-
return unless method_name
|
|
139
|
-
|
|
140
|
-
send(method_name, argument)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def argument_parameter_fragment(argument)
|
|
144
|
-
argument.children[0].to_s
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def optional_parameter_fragment(argument)
|
|
148
|
-
"#{argument.children[0]} = #{compile_safe_unparse(argument.children[1])}"
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def rest_parameter_fragment(argument)
|
|
152
|
-
prefixed_parameter(argument, "*")
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def keyword_parameter_fragment(argument)
|
|
156
|
-
"#{argument.children[0]}:"
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def optional_keyword_parameter_fragment(argument)
|
|
160
|
-
"#{argument.children[0]}: #{compile_safe_unparse(argument.children[1])}"
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def keyword_rest_parameter_fragment(argument)
|
|
164
|
-
prefixed_parameter(argument, "**")
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def block_parameter_fragment(argument)
|
|
168
|
-
"&#{argument.children[0]}"
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def forward_parameter_fragment(_argument)
|
|
172
|
-
"*args, **kwargs, &block"
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def prefixed_parameter(argument, prefix)
|
|
176
|
-
name = argument.children[0]
|
|
177
|
-
name ? "#{prefix}#{name}" : prefix
|
|
115
|
+
ParameterSource.new.build(mutant.subject.ast_node)
|
|
178
116
|
end
|
|
179
117
|
|
|
180
118
|
def block_body(subject_node)
|
|
181
119
|
subject_node.children[2]
|
|
182
120
|
end
|
|
183
121
|
|
|
184
|
-
def block_arguments(subject_node)
|
|
185
|
-
subject_node.children[1]
|
|
186
|
-
end
|
|
187
|
-
|
|
188
122
|
def heredoc_location?(location)
|
|
189
123
|
location.respond_to?(:heredoc_body) && location.heredoc_body
|
|
190
124
|
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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require_relative "mutant_identity"
|
|
4
5
|
|
|
5
6
|
module Henitai
|
|
6
7
|
# Represents a single syntactic mutation applied to a Subject.
|
|
@@ -16,6 +17,7 @@ module Henitai
|
|
|
16
17
|
# :ignored, :no_coverage
|
|
17
18
|
class Mutant
|
|
18
19
|
autoload :Activator, "henitai/mutant/activator"
|
|
20
|
+
autoload :ParameterSource, "henitai/mutant/parameter_source"
|
|
19
21
|
|
|
20
22
|
# Status-Vokabular folgt dem Stryker mutation-testing-report-schema.
|
|
21
23
|
# :equivalent ist ein Henitai-interner Status (wird im JSON als "Ignored" serialisiert,
|
|
@@ -34,7 +36,8 @@ module Henitai
|
|
|
34
36
|
].freeze
|
|
35
37
|
|
|
36
38
|
attr_reader :id, :subject, :operator, :original_node, :mutated_node,
|
|
37
|
-
:mutation_type, :description, :location
|
|
39
|
+
:mutation_type, :description, :location,
|
|
40
|
+
:precomputed_stable_id, :precomputed_activation_source
|
|
38
41
|
attr_accessor :status, :killing_test, :duration, :covered_by, :tests_completed
|
|
39
42
|
|
|
40
43
|
# @param subject [Subject] the subject being mutated
|
|
@@ -42,7 +45,9 @@ module Henitai
|
|
|
42
45
|
# @param nodes [Hash] AST nodes with :original and :mutated entries
|
|
43
46
|
# @param description [String] human-readable description of the mutation
|
|
44
47
|
# @param location [Hash] { file:, start_line:, end_line:, start_col:, end_col: }
|
|
45
|
-
|
|
48
|
+
# rubocop:disable Metrics/ParameterLists
|
|
49
|
+
def initialize(subject:, operator:, nodes:, description:, location:,
|
|
50
|
+
precomputed_stable_id: nil, precomputed_activation_source: nil)
|
|
46
51
|
@id = SecureRandom.uuid
|
|
47
52
|
@subject = subject
|
|
48
53
|
@operator = operator
|
|
@@ -50,12 +55,19 @@ module Henitai
|
|
|
50
55
|
@mutated_node = nodes.fetch(:mutated)
|
|
51
56
|
@description = description
|
|
52
57
|
@location = location
|
|
58
|
+
@precomputed_stable_id = precomputed_stable_id
|
|
59
|
+
@precomputed_activation_source = precomputed_activation_source
|
|
53
60
|
@status = :pending
|
|
54
61
|
@killing_test = nil
|
|
55
62
|
@duration = nil
|
|
56
63
|
@covered_by = nil
|
|
57
64
|
@tests_completed = nil
|
|
58
65
|
end
|
|
66
|
+
# rubocop:enable Metrics/ParameterLists
|
|
67
|
+
|
|
68
|
+
def stable_id
|
|
69
|
+
@stable_id ||= @precomputed_stable_id || MutantIdentity.stable_id(self)
|
|
70
|
+
end
|
|
59
71
|
|
|
60
72
|
def killed? = @status == :killed
|
|
61
73
|
def survived? = @status == :survived
|
|
@@ -68,15 +68,34 @@ module Henitai
|
|
|
68
68
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
|
-
def walk(node)
|
|
71
|
+
def walk(node, parent: nil)
|
|
72
72
|
return unless node.is_a?(Parser::AST::Node)
|
|
73
73
|
|
|
74
|
+
# Str children of a non-heredoc dstr are raw text segments embedded
|
|
75
|
+
# inside a quoted interpolated string. They have no surrounding quotes
|
|
76
|
+
# in the source, so replacing them via source-fragment substitution
|
|
77
|
+
# would insert a quoted literal into the raw-text position and produce
|
|
78
|
+
# a SyntaxError when the mutant is activated.
|
|
79
|
+
# Heredoc dstr children are exempt: the heredoc body is plain text, so
|
|
80
|
+
# inserting "" there stays valid Ruby.
|
|
81
|
+
return if embedded_non_heredoc_dstr_str?(node, parent)
|
|
82
|
+
|
|
74
83
|
apply_operators(node) if node_within_subject_range?(node)
|
|
75
84
|
node.children.each do |child|
|
|
76
|
-
walk(child)
|
|
85
|
+
walk(child, parent: node)
|
|
77
86
|
end
|
|
78
87
|
end
|
|
79
88
|
|
|
89
|
+
def embedded_non_heredoc_dstr_str?(node, parent)
|
|
90
|
+
node.type == :str &&
|
|
91
|
+
parent&.type == :dstr &&
|
|
92
|
+
!heredoc_node?(parent)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def heredoc_node?(node)
|
|
96
|
+
node.location.respond_to?(:heredoc_body) && node.location.heredoc_body
|
|
97
|
+
end
|
|
98
|
+
|
|
80
99
|
def apply_operators(node)
|
|
81
100
|
return if @arid_node_filter.suppressed?(node, @config)
|
|
82
101
|
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
UPSERT_MUTANT = <<~SQL
|
|
53
|
+
INSERT INTO mutants (
|
|
54
|
+
mutant_id,
|
|
55
|
+
first_seen_version,
|
|
56
|
+
first_seen_at,
|
|
57
|
+
last_seen_version,
|
|
58
|
+
last_seen_at,
|
|
59
|
+
current_status,
|
|
60
|
+
status_history,
|
|
61
|
+
days_alive
|
|
62
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
63
|
+
ON CONFLICT(mutant_id) DO UPDATE SET
|
|
64
|
+
last_seen_version = excluded.last_seen_version,
|
|
65
|
+
last_seen_at = excluded.last_seen_at,
|
|
66
|
+
current_status = excluded.current_status,
|
|
67
|
+
status_history = excluded.status_history,
|
|
68
|
+
days_alive = excluded.days_alive
|
|
69
|
+
SQL
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -1,80 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "date"
|
|
4
|
-
require "digest"
|
|
5
4
|
require "fileutils"
|
|
6
5
|
require "json"
|
|
7
6
|
require "sqlite3"
|
|
8
7
|
require "time"
|
|
9
|
-
|
|
8
|
+
require_relative "mutant_identity"
|
|
9
|
+
require_relative "mutant_history_store/sql"
|
|
10
10
|
|
|
11
11
|
module Henitai
|
|
12
12
|
# Persists mutant outcomes across runs in a lightweight SQLite database.
|
|
13
|
-
# rubocop:disable Metrics/ClassLength
|
|
14
13
|
class MutantHistoryStore
|
|
15
|
-
RUNS_TABLE_SQL = <<~SQL
|
|
16
|
-
CREATE TABLE IF NOT EXISTS runs (
|
|
17
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
|
-
version TEXT NOT NULL,
|
|
19
|
-
recorded_at TEXT NOT NULL,
|
|
20
|
-
mutation_score REAL,
|
|
21
|
-
mutation_score_indicator REAL,
|
|
22
|
-
equivalence_uncertainty TEXT,
|
|
23
|
-
total_mutants INTEGER NOT NULL,
|
|
24
|
-
killed_mutants INTEGER NOT NULL,
|
|
25
|
-
survived_mutants INTEGER NOT NULL,
|
|
26
|
-
timeout_mutants INTEGER NOT NULL,
|
|
27
|
-
equivalent_mutants INTEGER NOT NULL
|
|
28
|
-
);
|
|
29
|
-
SQL
|
|
30
|
-
|
|
31
|
-
MUTANTS_TABLE_SQL = <<~SQL
|
|
32
|
-
CREATE TABLE IF NOT EXISTS mutants (
|
|
33
|
-
mutant_id TEXT PRIMARY KEY,
|
|
34
|
-
first_seen_version TEXT NOT NULL,
|
|
35
|
-
first_seen_at TEXT NOT NULL,
|
|
36
|
-
last_seen_version TEXT NOT NULL,
|
|
37
|
-
last_seen_at TEXT NOT NULL,
|
|
38
|
-
current_status TEXT NOT NULL,
|
|
39
|
-
status_history TEXT NOT NULL,
|
|
40
|
-
days_alive INTEGER NOT NULL
|
|
41
|
-
);
|
|
42
|
-
SQL
|
|
43
|
-
|
|
44
|
-
INSERT_RUN_SQL = <<~SQL
|
|
45
|
-
INSERT INTO runs (
|
|
46
|
-
version,
|
|
47
|
-
recorded_at,
|
|
48
|
-
mutation_score,
|
|
49
|
-
mutation_score_indicator,
|
|
50
|
-
equivalence_uncertainty,
|
|
51
|
-
total_mutants,
|
|
52
|
-
killed_mutants,
|
|
53
|
-
survived_mutants,
|
|
54
|
-
timeout_mutants,
|
|
55
|
-
equivalent_mutants
|
|
56
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
57
|
-
SQL
|
|
58
|
-
|
|
59
|
-
UPSERT_MUTANT_SQL = <<~SQL
|
|
60
|
-
INSERT INTO mutants (
|
|
61
|
-
mutant_id,
|
|
62
|
-
first_seen_version,
|
|
63
|
-
first_seen_at,
|
|
64
|
-
last_seen_version,
|
|
65
|
-
last_seen_at,
|
|
66
|
-
current_status,
|
|
67
|
-
status_history,
|
|
68
|
-
days_alive
|
|
69
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
70
|
-
ON CONFLICT(mutant_id) DO UPDATE SET
|
|
71
|
-
last_seen_version = excluded.last_seen_version,
|
|
72
|
-
last_seen_at = excluded.last_seen_at,
|
|
73
|
-
current_status = excluded.current_status,
|
|
74
|
-
status_history = excluded.status_history,
|
|
75
|
-
days_alive = excluded.days_alive
|
|
76
|
-
SQL
|
|
77
|
-
|
|
78
14
|
def initialize(path:)
|
|
79
15
|
@path = path
|
|
80
16
|
end
|
|
@@ -87,7 +23,7 @@ module Henitai
|
|
|
87
23
|
with_database do |db|
|
|
88
24
|
ensure_schema(db)
|
|
89
25
|
db.transaction do
|
|
90
|
-
insert_run(db, result, version, recorded_at)
|
|
26
|
+
insert_run(db, result, version, recorded_at) unless partial_rerun?(result)
|
|
91
27
|
Array(result.mutants).each do |mutant|
|
|
92
28
|
upsert_mutant(db, mutant, version, recorded_at)
|
|
93
29
|
end
|
|
@@ -108,6 +44,10 @@ module Henitai
|
|
|
108
44
|
|
|
109
45
|
private
|
|
110
46
|
|
|
47
|
+
def partial_rerun?(result)
|
|
48
|
+
result.respond_to?(:partial_rerun?) && result.partial_rerun?
|
|
49
|
+
end
|
|
50
|
+
|
|
111
51
|
def with_database
|
|
112
52
|
db = SQLite3::Database.new(path)
|
|
113
53
|
db.results_as_hash = true
|
|
@@ -117,17 +57,17 @@ module Henitai
|
|
|
117
57
|
end
|
|
118
58
|
|
|
119
59
|
def ensure_schema(db)
|
|
120
|
-
db.execute_batch(
|
|
121
|
-
db.execute_batch(
|
|
60
|
+
db.execute_batch(Sql::RUNS_TABLE)
|
|
61
|
+
db.execute_batch(Sql::MUTANTS_TABLE)
|
|
122
62
|
end
|
|
123
63
|
|
|
124
64
|
def insert_run(db, result, version, recorded_at)
|
|
125
|
-
db.execute(
|
|
65
|
+
db.execute(Sql::INSERT_RUN, insert_run_bindings(result, version, recorded_at))
|
|
126
66
|
end
|
|
127
67
|
|
|
128
68
|
def upsert_mutant(db, mutant, version, recorded_at)
|
|
129
69
|
db.execute(
|
|
130
|
-
|
|
70
|
+
Sql::UPSERT_MUTANT,
|
|
131
71
|
upsert_mutant_bindings(mutant_history_data(db, mutant, version, recorded_at))
|
|
132
72
|
)
|
|
133
73
|
end
|
|
@@ -140,25 +80,7 @@ module Henitai
|
|
|
140
80
|
end
|
|
141
81
|
|
|
142
82
|
def stable_mutant_id(mutant)
|
|
143
|
-
|
|
144
|
-
[
|
|
145
|
-
mutant.subject.expression,
|
|
146
|
-
mutant.operator,
|
|
147
|
-
mutant.description,
|
|
148
|
-
mutant.location[:file],
|
|
149
|
-
mutant.location[:start_line],
|
|
150
|
-
mutant.location[:end_line],
|
|
151
|
-
mutant.location[:start_col],
|
|
152
|
-
mutant.location[:end_col],
|
|
153
|
-
mutation_signature(mutant)
|
|
154
|
-
].join("\0")
|
|
155
|
-
)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def mutation_signature(mutant)
|
|
159
|
-
Unparser.unparse(mutant.mutated_node)
|
|
160
|
-
rescue StandardError
|
|
161
|
-
mutant.mutated_node.class.name
|
|
83
|
+
MutantIdentity.stable_id(mutant)
|
|
162
84
|
end
|
|
163
85
|
|
|
164
86
|
def mutation_history_entry(mutant, version, recorded_at)
|
|
@@ -276,4 +198,3 @@ module Henitai
|
|
|
276
198
|
end
|
|
277
199
|
end
|
|
278
200
|
end
|
|
279
|
-
# rubocop:enable Metrics/ClassLength
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "unparser"
|
|
5
|
+
|
|
6
|
+
module Henitai
|
|
7
|
+
# Computes a stable, run-independent SHA256 identity for a mutant.
|
|
8
|
+
#
|
|
9
|
+
# The identity is derived from the mutant's semantic content, not the
|
|
10
|
+
# session UUID or source coordinates, so it survives ordinary line shifts.
|
|
11
|
+
module MutantIdentity
|
|
12
|
+
def self.stable_id(mutant)
|
|
13
|
+
Digest::SHA256.hexdigest(identity_components(mutant).join("\0"))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.identity_components(mutant)
|
|
17
|
+
[
|
|
18
|
+
mutant.subject.expression,
|
|
19
|
+
mutant.operator,
|
|
20
|
+
mutant.description,
|
|
21
|
+
mutant.location[:file],
|
|
22
|
+
mutation_signature(mutant)
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
private_class_method :identity_components
|
|
26
|
+
|
|
27
|
+
def self.mutation_signature(mutant)
|
|
28
|
+
Unparser.unparse(mutant.mutated_node)
|
|
29
|
+
rescue StandardError
|
|
30
|
+
mutant.mutated_node.class.name
|
|
31
|
+
end
|
|
32
|
+
private_class_method :mutation_signature
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -31,15 +31,26 @@ module Henitai
|
|
|
31
31
|
start_parallel_stdin_watcher(context, stdin_pipe)
|
|
32
32
|
parallel_workers(context, process_mutant).each(&:join)
|
|
33
33
|
ensure
|
|
34
|
+
teardown_parallel_execution(context)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :worker_count
|
|
40
|
+
|
|
41
|
+
def teardown_parallel_execution(context)
|
|
34
42
|
stop_parallel_stdin_watcher(context)
|
|
35
43
|
restore_parallel_signal_traps(context)
|
|
44
|
+
emit_scheduler_diagnostics if Integration::SchedulerDiagnostics.enabled?
|
|
36
45
|
raise context.state[:error] if context&.state&.fetch(:error, nil)
|
|
37
46
|
raise Interrupt if context&.state&.fetch(:stopping, false)
|
|
38
47
|
end
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
def emit_scheduler_diagnostics
|
|
50
|
+
summary = Integration::SchedulerDiagnostics.summary
|
|
51
|
+
warn "[henitai-scheduler] max_concurrent_children=#{summary[:max_concurrent]}"
|
|
52
|
+
warn "[henitai-scheduler] child_intervals=#{summary[:intervals].inspect}"
|
|
53
|
+
end
|
|
43
54
|
|
|
44
55
|
def build_parallel_queue(mutants)
|
|
45
56
|
Queue.new.tap { |queue| mutants.each { |mutant| queue << mutant } }
|
|
@@ -84,20 +95,16 @@ module Henitai
|
|
|
84
95
|
end
|
|
85
96
|
|
|
86
97
|
def parallel_workers(context, process_mutant)
|
|
87
|
-
Array.new(worker_count)
|
|
98
|
+
Array.new(worker_count) do
|
|
99
|
+
Thread.new { process_parallel_worker(context, process_mutant) }
|
|
100
|
+
end
|
|
88
101
|
end
|
|
89
102
|
|
|
90
103
|
def process_parallel_worker(context, process_mutant)
|
|
91
104
|
loop do
|
|
92
105
|
break if context.state[:stopping]
|
|
93
106
|
|
|
94
|
-
process_mutant
|
|
95
|
-
context.queue.pop(true),
|
|
96
|
-
context.integration,
|
|
97
|
-
context.config,
|
|
98
|
-
context.progress_reporter,
|
|
99
|
-
context.mutex
|
|
100
|
-
)
|
|
107
|
+
run_one_mutant(context, process_mutant)
|
|
101
108
|
rescue ThreadError
|
|
102
109
|
break
|
|
103
110
|
rescue StandardError => e
|
|
@@ -106,6 +113,17 @@ module Henitai
|
|
|
106
113
|
end
|
|
107
114
|
end
|
|
108
115
|
|
|
116
|
+
def run_one_mutant(context, process_mutant)
|
|
117
|
+
mutant = context.queue.pop(true)
|
|
118
|
+
process_mutant.call(
|
|
119
|
+
mutant,
|
|
120
|
+
context.integration,
|
|
121
|
+
context.config,
|
|
122
|
+
context.progress_reporter,
|
|
123
|
+
context.mutex
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
109
127
|
def stop_parallel_stdin_watcher(context)
|
|
110
128
|
context&.stdin_watcher&.kill
|
|
111
129
|
end
|
|
@@ -53,7 +53,9 @@ module Henitai
|
|
|
53
53
|
|
|
54
54
|
def current_snapshot
|
|
55
55
|
Coverage.peek_result
|
|
56
|
-
rescue
|
|
56
|
+
rescue RuntimeError
|
|
57
|
+
# Raised when coverage measurement is not running; caller warns and skips.
|
|
58
|
+
# Other errors are genuine bugs and must surface.
|
|
57
59
|
nil
|
|
58
60
|
end
|
|
59
61
|
|