henitai 0.2.0 → 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 +26 -1
- data/README.md +15 -3
- 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 +16 -404
- data/lib/henitai/configuration.rb +2 -1
- 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/eager_load.rb +36 -5
- data/lib/henitai/execution_engine.rb +4 -3
- 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_test_selection.rb +135 -0
- data/lib/henitai/integration/scenario_log_support.rb +116 -0
- data/lib/henitai/integration.rb +22 -846
- data/lib/henitai/mutant/activator.rb +1 -79
- data/lib/henitai/mutant/parameter_source.rb +98 -0
- data/lib/henitai/mutant.rb +1 -0
- data/lib/henitai/mutant_history_store/sql.rb +72 -0
- data/lib/henitai/mutant_history_store.rb +5 -69
- data/lib/henitai/per_test_coverage_collector.rb +3 -1
- data/lib/henitai/process_worker_runner.rb +48 -334
- data/lib/henitai/reporter.rb +20 -8
- data/lib/henitai/result.rb +17 -15
- data/lib/henitai/runner.rb +59 -182
- 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/survivor_rerun_strategy.rb +195 -0
- data/lib/henitai/unparse_helper.rb +5 -2
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +2 -0
- data/sig/configuration_validator.rbs +46 -22
- data/sig/henitai.rbs +158 -73
- metadata +25 -2
|
@@ -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
|
|
@@ -124,78 +112,13 @@ module Henitai
|
|
|
124
112
|
end
|
|
125
113
|
|
|
126
114
|
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
|
|
115
|
+
ParameterSource.new.build(mutant.subject.ast_node)
|
|
189
116
|
end
|
|
190
117
|
|
|
191
118
|
def block_body(subject_node)
|
|
192
119
|
subject_node.children[2]
|
|
193
120
|
end
|
|
194
121
|
|
|
195
|
-
def block_arguments(subject_node)
|
|
196
|
-
subject_node.children[1]
|
|
197
|
-
end
|
|
198
|
-
|
|
199
122
|
def heredoc_location?(location)
|
|
200
123
|
location.respond_to?(:heredoc_body) && location.heredoc_body
|
|
201
124
|
end
|
|
@@ -277,6 +200,5 @@ module Henitai
|
|
|
277
200
|
raise Unparser::UnsupportedNodeError, e.message
|
|
278
201
|
end
|
|
279
202
|
end
|
|
280
|
-
# rubocop:enable Metrics/ClassLength
|
|
281
203
|
end
|
|
282
204
|
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
|
@@ -17,6 +17,7 @@ module Henitai
|
|
|
17
17
|
# :ignored, :no_coverage
|
|
18
18
|
class Mutant
|
|
19
19
|
autoload :Activator, "henitai/mutant/activator"
|
|
20
|
+
autoload :ParameterSource, "henitai/mutant/parameter_source"
|
|
20
21
|
|
|
21
22
|
# Status-Vokabular folgt dem Stryker mutation-testing-report-schema.
|
|
22
23
|
# :equivalent ist ein Henitai-interner Status (wird im JSON als "Ignored" serialisiert,
|
|
@@ -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
|
|
@@ -6,74 +6,11 @@ require "json"
|
|
|
6
6
|
require "sqlite3"
|
|
7
7
|
require "time"
|
|
8
8
|
require_relative "mutant_identity"
|
|
9
|
+
require_relative "mutant_history_store/sql"
|
|
9
10
|
|
|
10
11
|
module Henitai
|
|
11
12
|
# Persists mutant outcomes across runs in a lightweight SQLite database.
|
|
12
|
-
# rubocop:disable Metrics/ClassLength
|
|
13
13
|
class MutantHistoryStore
|
|
14
|
-
RUNS_TABLE_SQL = <<~SQL
|
|
15
|
-
CREATE TABLE IF NOT EXISTS runs (
|
|
16
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
-
version TEXT NOT NULL,
|
|
18
|
-
recorded_at TEXT NOT NULL,
|
|
19
|
-
mutation_score REAL,
|
|
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
14
|
def initialize(path:)
|
|
78
15
|
@path = path
|
|
79
16
|
end
|
|
@@ -120,17 +57,17 @@ module Henitai
|
|
|
120
57
|
end
|
|
121
58
|
|
|
122
59
|
def ensure_schema(db)
|
|
123
|
-
db.execute_batch(
|
|
124
|
-
db.execute_batch(
|
|
60
|
+
db.execute_batch(Sql::RUNS_TABLE)
|
|
61
|
+
db.execute_batch(Sql::MUTANTS_TABLE)
|
|
125
62
|
end
|
|
126
63
|
|
|
127
64
|
def insert_run(db, result, version, recorded_at)
|
|
128
|
-
db.execute(
|
|
65
|
+
db.execute(Sql::INSERT_RUN, insert_run_bindings(result, version, recorded_at))
|
|
129
66
|
end
|
|
130
67
|
|
|
131
68
|
def upsert_mutant(db, mutant, version, recorded_at)
|
|
132
69
|
db.execute(
|
|
133
|
-
|
|
70
|
+
Sql::UPSERT_MUTANT,
|
|
134
71
|
upsert_mutant_bindings(mutant_history_data(db, mutant, version, recorded_at))
|
|
135
72
|
)
|
|
136
73
|
end
|
|
@@ -261,4 +198,3 @@ module Henitai
|
|
|
261
198
|
end
|
|
262
199
|
end
|
|
263
200
|
end
|
|
264
|
-
# rubocop:enable Metrics/ClassLength
|
|
@@ -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
|
|