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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -1
  3. data/README.md +15 -3
  4. data/assets/schema/henitai.schema.json +6 -0
  5. data/lib/henitai/cli/clean_command.rb +48 -0
  6. data/lib/henitai/cli/command_support.rb +51 -0
  7. data/lib/henitai/cli/init_command.rb +64 -0
  8. data/lib/henitai/cli/operator_command.rb +95 -0
  9. data/lib/henitai/cli/options.rb +120 -0
  10. data/lib/henitai/cli/run_command.rb +103 -0
  11. data/lib/henitai/cli.rb +16 -404
  12. data/lib/henitai/configuration.rb +2 -1
  13. data/lib/henitai/configuration_validator/rules.rb +143 -0
  14. data/lib/henitai/configuration_validator/scalars.rb +123 -0
  15. data/lib/henitai/configuration_validator.rb +12 -239
  16. data/lib/henitai/eager_load.rb +36 -5
  17. data/lib/henitai/execution_engine.rb +4 -3
  18. data/lib/henitai/integration/base.rb +171 -0
  19. data/lib/henitai/integration/child_debug_support.rb +115 -0
  20. data/lib/henitai/integration/child_runtime_control.rb +50 -0
  21. data/lib/henitai/integration/coverage_suppression.rb +43 -0
  22. data/lib/henitai/integration/minitest.rb +133 -0
  23. data/lib/henitai/integration/mutant_run_support.rb +77 -0
  24. data/lib/henitai/integration/rspec_child_runner.rb +61 -0
  25. data/lib/henitai/integration/rspec_test_selection.rb +135 -0
  26. data/lib/henitai/integration/scenario_log_support.rb +116 -0
  27. data/lib/henitai/integration.rb +22 -846
  28. data/lib/henitai/mutant/activator.rb +1 -79
  29. data/lib/henitai/mutant/parameter_source.rb +98 -0
  30. data/lib/henitai/mutant.rb +1 -0
  31. data/lib/henitai/mutant_history_store/sql.rb +72 -0
  32. data/lib/henitai/mutant_history_store.rb +5 -69
  33. data/lib/henitai/per_test_coverage_collector.rb +3 -1
  34. data/lib/henitai/process_worker_runner.rb +48 -334
  35. data/lib/henitai/reporter.rb +20 -8
  36. data/lib/henitai/result.rb +17 -15
  37. data/lib/henitai/runner.rb +59 -182
  38. data/lib/henitai/slot_scheduler/draining.rb +140 -0
  39. data/lib/henitai/slot_scheduler/process_control.rb +43 -0
  40. data/lib/henitai/slot_scheduler.rb +214 -0
  41. data/lib/henitai/survivor_rerun_strategy.rb +195 -0
  42. data/lib/henitai/unparse_helper.rb +5 -2
  43. data/lib/henitai/version.rb +1 -1
  44. data/lib/henitai.rb +2 -0
  45. data/sig/configuration_validator.rbs +46 -22
  46. data/sig/henitai.rbs +158 -73
  47. 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
- args_node = method_arguments(mutant.subject.ast_node)
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
@@ -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(RUNS_TABLE_SQL)
124
- db.execute_batch(MUTANTS_TABLE_SQL)
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(INSERT_RUN_SQL, insert_run_bindings(result, version, recorded_at))
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
- UPSERT_MUTANT_SQL,
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 StandardError
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