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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -1
  3. data/README.md +138 -5
  4. data/assets/schema/henitai.schema.json +41 -0
  5. data/lib/henitai/available_cpu_count.rb +16 -23
  6. data/lib/henitai/canonical_report_merger.rb +107 -0
  7. data/lib/henitai/canonical_report_writer.rb +22 -0
  8. data/lib/henitai/checkpoint_reporter.rb +79 -0
  9. data/lib/henitai/cli/clean_command.rb +54 -0
  10. data/lib/henitai/cli/command_support.rb +52 -0
  11. data/lib/henitai/cli/init_command.rb +64 -0
  12. data/lib/henitai/cli/operator_command.rb +96 -0
  13. data/lib/henitai/cli/options.rb +84 -0
  14. data/lib/henitai/cli/run_command.rb +136 -0
  15. data/lib/henitai/cli/run_options.rb +108 -0
  16. data/lib/henitai/cli.rb +27 -407
  17. data/lib/henitai/composite_progress_reporter.rb +42 -0
  18. data/lib/henitai/configuration.rb +42 -9
  19. data/lib/henitai/configuration_validator/rules.rb +161 -0
  20. data/lib/henitai/configuration_validator/scalars.rb +169 -0
  21. data/lib/henitai/configuration_validator.rb +20 -240
  22. data/lib/henitai/coverage_bootstrapper.rb +54 -5
  23. data/lib/henitai/coverage_formatter.rb +5 -1
  24. data/lib/henitai/coverage_report_reader.rb +28 -5
  25. data/lib/henitai/eager_load.rb +36 -5
  26. data/lib/henitai/execution_engine/env_scope.rb +67 -0
  27. data/lib/henitai/execution_engine.rb +115 -53
  28. data/lib/henitai/generated_artifacts.rb +58 -0
  29. data/lib/henitai/incremental_filter.rb +100 -0
  30. data/lib/henitai/integration/base.rb +171 -0
  31. data/lib/henitai/integration/child_debug_support.rb +119 -0
  32. data/lib/henitai/integration/child_runtime_control.rb +50 -0
  33. data/lib/henitai/integration/coverage_suppression.rb +52 -0
  34. data/lib/henitai/integration/minitest.rb +101 -0
  35. data/lib/henitai/integration/minitest_load_path.rb +14 -0
  36. data/lib/henitai/integration/minitest_suite_command.rb +17 -0
  37. data/lib/henitai/integration/minitest_test_runner.rb +39 -0
  38. data/lib/henitai/integration/mutant_run_support.rb +77 -0
  39. data/lib/henitai/integration/rails_environment_preloader.rb +14 -0
  40. data/lib/henitai/integration/rspec_child_runner.rb +61 -0
  41. data/lib/henitai/integration/rspec_test_selection.rb +138 -0
  42. data/lib/henitai/integration/scenario_log_support.rb +160 -0
  43. data/lib/henitai/integration.rb +22 -846
  44. data/lib/henitai/minitest_coverage_reporter.rb +4 -1
  45. data/lib/henitai/mutant/activator.rb +15 -79
  46. data/lib/henitai/mutant/parameter_source.rb +98 -0
  47. data/lib/henitai/mutant.rb +14 -6
  48. data/lib/henitai/mutant_history_store/sql.rb +90 -0
  49. data/lib/henitai/mutant_history_store/verdict_cache.rb +84 -0
  50. data/lib/henitai/mutant_history_store.rb +67 -81
  51. data/lib/henitai/mutant_identity.rb +33 -2
  52. data/lib/henitai/mutation_skip_directives.rb +227 -0
  53. data/lib/henitai/operator.rb +3 -1
  54. data/lib/henitai/operators/equality_identity_operator.rb +51 -0
  55. data/lib/henitai/operators/equality_operator.rb +8 -3
  56. data/lib/henitai/operators.rb +1 -0
  57. data/lib/henitai/per_test_coverage.rb +66 -0
  58. data/lib/henitai/per_test_coverage_collector.rb +23 -6
  59. data/lib/henitai/per_test_coverage_selector.rb +11 -33
  60. data/lib/henitai/process_worker_runner.rb +48 -334
  61. data/lib/henitai/reporter/dashboard_metadata_provider.rb +113 -0
  62. data/lib/henitai/reporter.rb +258 -125
  63. data/lib/henitai/reports_directory_lock.rb +76 -0
  64. data/lib/henitai/result.rb +88 -23
  65. data/lib/henitai/runner.rb +137 -218
  66. data/lib/henitai/scenario_execution_result.rb +12 -0
  67. data/lib/henitai/slot_scheduler/draining.rb +146 -0
  68. data/lib/henitai/slot_scheduler/process_control.rb +43 -0
  69. data/lib/henitai/slot_scheduler.rb +272 -0
  70. data/lib/henitai/static_filter.rb +16 -6
  71. data/lib/henitai/survivor_rerun_strategy.rb +195 -0
  72. data/lib/henitai/survivor_test_filter.rb +1 -1
  73. data/lib/henitai/test_prioritizer.rb +28 -2
  74. data/lib/henitai/timeout_calibrator.rb +44 -0
  75. data/lib/henitai/unparse_helper.rb +5 -2
  76. data/lib/henitai/verdict_fingerprint.rb +155 -0
  77. data/lib/henitai/version.rb +1 -1
  78. data/lib/henitai.rb +16 -1
  79. data/sig/configuration_validator.rbs +46 -22
  80. data/sig/henitai.rbs +386 -96
  81. metadata +45 -3
  82. data/lib/henitai/parallel_execution_runner.rb +0 -153
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "scalars"
4
+
5
+ module Henitai
6
+ module ConfigurationValidator
7
+ # Section-level validation rules.
8
+ #
9
+ # Each +validate_*+ method inspects one configuration section, warns about
10
+ # unknown keys via {ConfigurationValidator.warn}, and delegates leaf value
11
+ # checks to {Scalars}. Failures raise +Henitai::ConfigurationError+.
12
+ module Rules
13
+ module_function
14
+
15
+ def validate_top_level_keys(raw)
16
+ warn_unknown_keys(raw, VALID_TOP_LEVEL_KEYS)
17
+ end
18
+
19
+ def validate_integration(raw)
20
+ value = raw[:integration]
21
+ return if value.nil?
22
+ return if value.is_a?(String)
23
+
24
+ ensure_hash!(value, "integration")
25
+ warn_unknown_keys(value, VALID_INTEGRATION_KEYS, "integration")
26
+ Scalars.validate_optional_string(value[:name], "integration.name")
27
+ end
28
+
29
+ def validate_includes(raw)
30
+ Scalars.validate_string_array(raw[:includes], "includes")
31
+ end
32
+
33
+ def validate_excludes(raw)
34
+ Scalars.validate_string_array(raw[:excludes], "excludes")
35
+ end
36
+
37
+ def validate_test_excludes(raw)
38
+ Scalars.validate_string_array(raw[:test_excludes], "test_excludes")
39
+ end
40
+
41
+ def validate_jobs(raw)
42
+ value = raw[:jobs]
43
+ return if value.nil?
44
+ return if value.is_a?(Integer)
45
+
46
+ configuration_error("Invalid configuration value for jobs: expected Integer, got #{value.class}")
47
+ end
48
+
49
+ def validate_reporters(raw)
50
+ Scalars.validate_string_array(raw[:reporters], "reporters")
51
+ end
52
+
53
+ def validate_reports_dir(raw)
54
+ Scalars.validate_optional_string(raw[:reports_dir], "reports_dir")
55
+ end
56
+
57
+ def validate_reports(raw)
58
+ value = raw[:reports]
59
+ return if value.nil?
60
+
61
+ ensure_hash!(value, "reports")
62
+ warn_unknown_keys(value, VALID_REPORTS_KEYS, "reports")
63
+ Scalars.validate_boolean(value[:checkpoint], "reports.checkpoint") unless value[:checkpoint].nil?
64
+ Scalars.validate_checkpoint_every(value[:checkpoint_every])
65
+ Scalars.validate_checkpoint_interval(value[:checkpoint_interval])
66
+ end
67
+
68
+ def validate_all_logs(raw)
69
+ value = raw[:all_logs]
70
+ return if value.nil?
71
+
72
+ Scalars.validate_boolean(value, "all_logs")
73
+ end
74
+
75
+ def validate_dashboard(raw)
76
+ value = raw[:dashboard]
77
+ return if value.nil?
78
+
79
+ ensure_hash!(value, "dashboard")
80
+ warn_unknown_keys(value, VALID_DASHBOARD_KEYS, "dashboard")
81
+ Scalars.validate_optional_string(value[:project], "dashboard.project")
82
+ Scalars.validate_optional_string(value[:base_url], "dashboard.base_url")
83
+ end
84
+
85
+ def validate_mutation(raw)
86
+ value = raw[:mutation]
87
+ return if value.nil?
88
+
89
+ ensure_hash!(value, "mutation")
90
+ warn_unknown_keys(value, VALID_MUTATION_KEYS, "mutation")
91
+ Scalars.validate_operator(value[:operators])
92
+ validate_mutation_limits(value)
93
+ validate_mutation_filters(value)
94
+ validate_sampling(value[:sampling])
95
+ end
96
+
97
+ def validate_mutation_limits(value)
98
+ Scalars.validate_timeout(value[:timeout])
99
+ Scalars.validate_timeout_multiplier(value[:timeout_multiplier])
100
+ Scalars.validate_max_flaky_retries(value[:max_flaky_retries])
101
+ Scalars.validate_max_log_bytes(value[:max_log_bytes])
102
+ Scalars.validate_max_timeout(value[:max_timeout])
103
+ end
104
+
105
+ def validate_mutation_filters(value)
106
+ Scalars.validate_string_array(value[:ignore_patterns], "mutation.ignore_patterns")
107
+ Scalars.validate_ignore_patterns(value[:ignore_patterns])
108
+ end
109
+
110
+ def validate_coverage_criteria(raw)
111
+ value = raw[:coverage_criteria]
112
+ return if value.nil?
113
+
114
+ ensure_hash!(value, "coverage_criteria")
115
+ warn_unknown_keys(value, VALID_COVERAGE_CRITERIA_KEYS, "coverage_criteria")
116
+ value.each { |key, flag| Scalars.validate_boolean(flag, "coverage_criteria.#{key}") }
117
+ end
118
+
119
+ def validate_thresholds(raw)
120
+ value = raw[:thresholds]
121
+ return if value.nil?
122
+
123
+ ensure_hash!(value, "thresholds")
124
+ warn_unknown_keys(value, VALID_THRESHOLDS_KEYS, "thresholds")
125
+ value.each { |key, threshold| Scalars.validate_threshold(threshold, "thresholds.#{key}") }
126
+ end
127
+
128
+ def validate_sampling(value)
129
+ return if value.nil?
130
+
131
+ ensure_hash!(value, "mutation.sampling")
132
+ warn_unknown_keys(value, VALID_SAMPLING_KEYS, "mutation.sampling")
133
+ Scalars.validate_sampling_completeness(value)
134
+ Scalars.validate_sampling_ratio(value[:ratio])
135
+ Scalars.validate_sampling_strategy(value[:strategy])
136
+ end
137
+
138
+ def warn_unknown_keys(raw, allowed_keys, path = nil)
139
+ raw.each_key do |key|
140
+ next if allowed_keys.include?(key)
141
+
142
+ ConfigurationValidator.warn "Unknown configuration key: #{key_path(path, key)}"
143
+ end
144
+ end
145
+
146
+ def key_path(path, key)
147
+ path ? "#{path}.#{key}" : key.to_s
148
+ end
149
+
150
+ def ensure_hash!(value, path)
151
+ return if value.is_a?(Hash)
152
+
153
+ configuration_error("Invalid configuration value for #{path}: expected Hash, got #{value.class}")
154
+ end
155
+
156
+ def configuration_error(message)
157
+ raise Henitai::ConfigurationError, message
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ module ConfigurationValidator
5
+ # Leaf validators for individual configuration values.
6
+ #
7
+ # Each method returns silently for an acceptable value or raises
8
+ # +Henitai::ConfigurationError+ via {Rules.configuration_error}.
9
+ module Scalars
10
+ module_function
11
+
12
+ def validate_operator(value)
13
+ return if value.nil?
14
+
15
+ operator = value.respond_to?(:to_sym) ? value.to_sym : nil
16
+ return if VALID_OPERATORS.include?(operator)
17
+
18
+ Rules.configuration_error(
19
+ "Invalid configuration value for mutation.operators: expected one of " \
20
+ "#{VALID_OPERATORS.join(', ')}, got #{value.inspect}"
21
+ )
22
+ end
23
+
24
+ def validate_timeout(value)
25
+ return if value.nil?
26
+ return if value.is_a?(Numeric)
27
+
28
+ Rules.configuration_error(
29
+ "Invalid configuration value for mutation.timeout: expected Numeric, got #{value.class}"
30
+ )
31
+ end
32
+
33
+ def validate_timeout_multiplier(value)
34
+ return if value.nil?
35
+ return if value.is_a?(Numeric) && value.positive?
36
+
37
+ Rules.configuration_error(
38
+ "Invalid configuration value for mutation.timeout_multiplier: " \
39
+ "expected positive Numeric, got #{value.inspect}"
40
+ )
41
+ end
42
+
43
+ def validate_threshold(value, path)
44
+ return if value.is_a?(Integer) && value.between?(0, 100)
45
+
46
+ Rules.configuration_error(
47
+ "Invalid configuration value for #{path}: expected Integer between 0 and 100, " \
48
+ "got #{value.inspect}"
49
+ )
50
+ end
51
+
52
+ def validate_boolean(value, path)
53
+ return if [true, false].include?(value)
54
+
55
+ Rules.configuration_error(
56
+ "Invalid configuration value for #{path}: expected true or false, got #{value.inspect}"
57
+ )
58
+ end
59
+
60
+ def validate_optional_string(value, path)
61
+ return if value.nil?
62
+ return if value.is_a?(String)
63
+
64
+ Rules.configuration_error("Invalid configuration value for #{path}: expected String, got #{value.class}")
65
+ end
66
+
67
+ def validate_string_array(value, path)
68
+ return if value.nil?
69
+ return if value.is_a?(Array) && value.all?(String)
70
+
71
+ Rules.configuration_error(
72
+ "Invalid configuration value for #{path}: expected Array<String>, got #{describe_array_type(value)}"
73
+ )
74
+ end
75
+
76
+ def validate_ignore_patterns(value)
77
+ Array(value).each do |pattern|
78
+ Regexp.new(pattern)
79
+ rescue RegexpError => e
80
+ Rules.configuration_error(
81
+ "Invalid configuration value for mutation.ignore_patterns: " \
82
+ "invalid regular expression #{pattern.inspect}: #{e.message}"
83
+ )
84
+ end
85
+ end
86
+
87
+ def validate_max_flaky_retries(value)
88
+ return if value.nil?
89
+ return if value.is_a?(Integer) && value >= 0
90
+
91
+ Rules.configuration_error(
92
+ "Invalid configuration value for mutation.max_flaky_retries: expected Integer >= 0, got #{value.inspect}"
93
+ )
94
+ end
95
+
96
+ def validate_max_log_bytes(value)
97
+ return if value.nil?
98
+ return if value.is_a?(Integer) && value.positive?
99
+
100
+ Rules.configuration_error(
101
+ "Invalid configuration value for mutation.max_log_bytes: expected Integer > 0, got #{value.inspect}"
102
+ )
103
+ end
104
+
105
+ def validate_max_timeout(value)
106
+ return if value.nil?
107
+ return if value.is_a?(Numeric) && value.positive?
108
+
109
+ Rules.configuration_error(
110
+ "Invalid configuration value for mutation.max_timeout: expected positive Numeric, got #{value.inspect}"
111
+ )
112
+ end
113
+
114
+ def validate_checkpoint_every(value)
115
+ return if value.nil?
116
+ return if value.is_a?(Integer) && value.positive?
117
+
118
+ Rules.configuration_error(
119
+ "Invalid configuration value for reports.checkpoint_every: expected Integer > 0, got #{value.inspect}"
120
+ )
121
+ end
122
+
123
+ def validate_checkpoint_interval(value)
124
+ return if value.nil?
125
+ return if value.is_a?(Numeric) && value.positive?
126
+
127
+ Rules.configuration_error(
128
+ "Invalid configuration value for reports.checkpoint_interval: expected positive Numeric, got #{value.inspect}"
129
+ )
130
+ end
131
+
132
+ def validate_sampling_ratio(value)
133
+ return if value.nil?
134
+ return if value.is_a?(Numeric) && value >= 0.0 && value <= 1.0
135
+
136
+ Rules.configuration_error(
137
+ "Invalid configuration value for mutation.sampling.ratio: " \
138
+ "expected Numeric between 0 and 1, got #{value.inspect}"
139
+ )
140
+ end
141
+
142
+ def validate_sampling_strategy(value)
143
+ return if value.nil?
144
+
145
+ strategy = value.respond_to?(:to_sym) ? value.to_sym : nil
146
+ return if strategy == :stratified
147
+
148
+ Rules.configuration_error(
149
+ "Invalid configuration value for mutation.sampling.strategy: expected stratified, got #{value.inspect}"
150
+ )
151
+ end
152
+
153
+ def validate_sampling_completeness(value)
154
+ return if value.key?(:ratio) && value.key?(:strategy)
155
+
156
+ Rules.configuration_error(
157
+ "Invalid configuration value for mutation.sampling: expected both ratio and strategy"
158
+ )
159
+ end
160
+
161
+ def describe_array_type(value)
162
+ return value.class.name unless value.is_a?(Array)
163
+
164
+ element_types = value.map { |item| item.class.name }.uniq.join(", ")
165
+ "Array<#{element_types}>"
166
+ end
167
+ end
168
+ end
169
+ end
@@ -1,22 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "configuration_validator/rules"
4
+
3
5
  module Henitai
4
- # rubocop:disable Metrics/ModuleLength
5
6
  # Internal validator for configuration data loaded from YAML and CLI overrides.
7
+ #
8
+ # The public entry point is {.validate!}; the individual rules live in
9
+ # {Rules}. Unknown-key warnings are emitted via {.warn} so that callers (and
10
+ # specs) can observe them on this module.
6
11
  module ConfigurationValidator
7
12
  VALID_TOP_LEVEL_KEYS = %i[
8
13
  integration
9
14
  includes
15
+ excludes
16
+ test_excludes
10
17
  mutation
11
18
  coverage_criteria
12
19
  thresholds
13
20
  reporters
14
21
  reports_dir
22
+ reports
15
23
  all_logs
16
24
  dashboard
17
25
  jobs
18
26
  ].freeze
19
- VALID_MUTATION_KEYS = %i[operators timeout ignore_patterns max_flaky_retries sampling].freeze
27
+ VALID_MUTATION_KEYS = %i[
28
+ operators timeout timeout_multiplier ignore_patterns max_flaky_retries max_log_bytes max_timeout sampling
29
+ ].freeze
30
+ VALID_REPORTS_KEYS = %i[checkpoint checkpoint_every checkpoint_interval].freeze
20
31
  VALID_SAMPLING_KEYS = %i[ratio strategy].freeze
21
32
  VALID_COVERAGE_CRITERIA_KEYS = %i[test_result timeout process_abort].freeze
22
33
  VALID_THRESHOLDS_KEYS = %i[high low].freeze
@@ -27,9 +38,12 @@ module Henitai
27
38
  validate_top_level_keys
28
39
  validate_integration
29
40
  validate_includes
41
+ validate_excludes
42
+ validate_test_excludes
30
43
  validate_jobs
31
44
  validate_reporters
32
45
  validate_reports_dir
46
+ validate_reports
33
47
  validate_all_logs
34
48
  validate_dashboard
35
49
  validate_mutation
@@ -38,246 +52,12 @@ module Henitai
38
52
  ].freeze
39
53
 
40
54
  def self.validate!(raw)
41
- ensure_hash!(raw, "configuration")
42
- VALIDATION_STEPS.each { |step| send(step, raw) }
55
+ Rules.ensure_hash!(raw, "configuration")
56
+ VALIDATION_STEPS.each { |step| Rules.public_send(step, raw) }
43
57
  end
44
58
 
45
- class << self
46
- private
47
-
48
- def validate_top_level_keys(raw)
49
- warn_unknown_keys(raw, VALID_TOP_LEVEL_KEYS)
50
- end
51
-
52
- def validate_integration(raw)
53
- value = raw[:integration]
54
- return if value.nil?
55
- return if value.is_a?(String)
56
-
57
- ensure_hash!(value, "integration")
58
- warn_unknown_keys(value, VALID_INTEGRATION_KEYS, "integration")
59
- validate_optional_string(value[:name], "integration.name")
60
- end
61
-
62
- def validate_includes(raw)
63
- validate_string_array(raw[:includes], "includes")
64
- end
65
-
66
- def validate_jobs(raw)
67
- value = raw[:jobs]
68
- return if value.nil?
69
- return if value.is_a?(Integer)
70
-
71
- configuration_error("Invalid configuration value for jobs: expected Integer, got #{value.class}")
72
- end
73
-
74
- def validate_reporters(raw)
75
- validate_string_array(raw[:reporters], "reporters")
76
- end
77
-
78
- def validate_reports_dir(raw)
79
- validate_optional_string(raw[:reports_dir], "reports_dir")
80
- end
81
-
82
- def validate_all_logs(raw)
83
- value = raw[:all_logs]
84
- return if value.nil?
85
-
86
- validate_boolean(value, "all_logs")
87
- end
88
-
89
- def validate_dashboard(raw)
90
- value = raw[:dashboard]
91
- return if value.nil?
92
-
93
- ensure_hash!(value, "dashboard")
94
- warn_unknown_keys(value, VALID_DASHBOARD_KEYS, "dashboard")
95
- validate_optional_string(value[:project], "dashboard.project")
96
- validate_optional_string(value[:base_url], "dashboard.base_url")
97
- end
98
-
99
- def validate_mutation(raw)
100
- value = raw[:mutation]
101
- return if value.nil?
102
-
103
- ensure_hash!(value, "mutation")
104
- warn_unknown_keys(value, VALID_MUTATION_KEYS, "mutation")
105
- validate_operator(value[:operators])
106
- validate_mutation_limits(value)
107
- validate_mutation_filters(value)
108
- validate_sampling(value[:sampling])
109
- end
110
-
111
- def validate_mutation_limits(value)
112
- validate_timeout(value[:timeout])
113
- validate_max_flaky_retries(value[:max_flaky_retries])
114
- end
115
-
116
- def validate_mutation_filters(value)
117
- validate_string_array(value[:ignore_patterns], "mutation.ignore_patterns")
118
- validate_ignore_patterns(value[:ignore_patterns])
119
- end
120
-
121
- def validate_coverage_criteria(raw)
122
- value = raw[:coverage_criteria]
123
- return if value.nil?
124
-
125
- ensure_hash!(value, "coverage_criteria")
126
- warn_unknown_keys(value, VALID_COVERAGE_CRITERIA_KEYS, "coverage_criteria")
127
- value.each do |key, flag|
128
- validate_boolean(flag, "coverage_criteria.#{key}")
129
- end
130
- end
131
-
132
- def validate_thresholds(raw)
133
- value = raw[:thresholds]
134
- return if value.nil?
135
-
136
- ensure_hash!(value, "thresholds")
137
- warn_unknown_keys(value, VALID_THRESHOLDS_KEYS, "thresholds")
138
- value.each do |key, threshold|
139
- validate_threshold(threshold, "thresholds.#{key}")
140
- end
141
- end
142
-
143
- def validate_operator(value)
144
- return if value.nil?
145
-
146
- operator = value.respond_to?(:to_sym) ? value.to_sym : nil
147
- return if VALID_OPERATORS.include?(operator)
148
-
149
- configuration_error(
150
- "Invalid configuration value for mutation.operators: expected one of " \
151
- "#{VALID_OPERATORS.join(', ')}, got #{value.inspect}"
152
- )
153
- end
154
-
155
- def validate_timeout(value)
156
- return if value.nil?
157
- return if value.is_a?(Numeric)
158
-
159
- configuration_error("Invalid configuration value for mutation.timeout: expected Numeric, got #{value.class}")
160
- end
161
-
162
- def validate_threshold(value, path)
163
- return if value.is_a?(Integer) && value.between?(0, 100)
164
-
165
- configuration_error(
166
- "Invalid configuration value for #{path}: expected Integer between 0 and 100, " \
167
- "got #{value.inspect}"
168
- )
169
- end
170
-
171
- def validate_boolean(value, path)
172
- return if [true, false].include?(value)
173
-
174
- configuration_error("Invalid configuration value for #{path}: expected true or false, got #{value.inspect}")
175
- end
176
-
177
- def validate_optional_string(value, path)
178
- return if value.nil?
179
- return if value.is_a?(String)
180
-
181
- configuration_error("Invalid configuration value for #{path}: expected String, got #{value.class}")
182
- end
183
-
184
- def validate_string_array(value, path)
185
- return if value.nil?
186
- return if value.is_a?(Array) && value.all?(String)
187
-
188
- configuration_error(
189
- "Invalid configuration value for #{path}: expected Array<String>, got #{describe_array_type(value)}"
190
- )
191
- end
192
-
193
- def validate_ignore_patterns(value)
194
- Array(value).each do |pattern|
195
- Regexp.new(pattern)
196
- rescue RegexpError => e
197
- configuration_error(
198
- "Invalid configuration value for mutation.ignore_patterns: " \
199
- "invalid regular expression #{pattern.inspect}: #{e.message}"
200
- )
201
- end
202
- end
203
-
204
- def validate_max_flaky_retries(value)
205
- return if value.nil?
206
- return if value.is_a?(Integer) && value >= 0
207
-
208
- configuration_error(
209
- "Invalid configuration value for mutation.max_flaky_retries: expected Integer >= 0, got #{value.inspect}"
210
- )
211
- end
212
-
213
- def validate_sampling(value)
214
- return if value.nil?
215
-
216
- ensure_hash!(value, "mutation.sampling")
217
- warn_unknown_keys(value, VALID_SAMPLING_KEYS, "mutation.sampling")
218
- validate_sampling_completeness(value)
219
- validate_sampling_ratio(value[:ratio])
220
- validate_sampling_strategy(value[:strategy])
221
- end
222
-
223
- def validate_sampling_ratio(value)
224
- return if value.nil?
225
- return if value.is_a?(Numeric) && value >= 0.0 && value <= 1.0
226
-
227
- configuration_error(
228
- "Invalid configuration value for mutation.sampling.ratio: " \
229
- "expected Numeric between 0 and 1, got #{value.inspect}"
230
- )
231
- end
232
-
233
- def validate_sampling_strategy(value)
234
- return if value.nil?
235
-
236
- strategy = value.respond_to?(:to_sym) ? value.to_sym : nil
237
- return if strategy == :stratified
238
-
239
- configuration_error(
240
- "Invalid configuration value for mutation.sampling.strategy: expected stratified, got #{value.inspect}"
241
- )
242
- end
243
-
244
- def validate_sampling_completeness(value)
245
- return if value.key?(:ratio) && value.key?(:strategy)
246
-
247
- configuration_error(
248
- "Invalid configuration value for mutation.sampling: expected both ratio and strategy"
249
- )
250
- end
251
-
252
- def warn_unknown_keys(raw, allowed_keys, path = nil)
253
- raw.each_key do |key|
254
- next if allowed_keys.include?(key)
255
-
256
- warn "Unknown configuration key: #{key_path(path, key)}"
257
- end
258
- end
259
-
260
- def key_path(path, key)
261
- path ? "#{path}.#{key}" : key.to_s
262
- end
263
-
264
- def ensure_hash!(value, path)
265
- return if value.is_a?(Hash)
266
-
267
- configuration_error("Invalid configuration value for #{path}: expected Hash, got #{value.class}")
268
- end
269
-
270
- def describe_array_type(value)
271
- return value.class.name unless value.is_a?(Array)
272
-
273
- element_types = value.map { |item| item.class.name }.uniq.join(", ")
274
- "Array<#{element_types}>"
275
- end
276
-
277
- def configuration_error(message)
278
- raise Henitai::ConfigurationError, message
279
- end
59
+ def self.warn(message)
60
+ Kernel.warn(message)
280
61
  end
281
62
  end
282
- # rubocop:enable Metrics/ModuleLength
283
63
  end