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
|
@@ -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[
|
|
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|
|
|
55
|
+
Rules.ensure_hash!(raw, "configuration")
|
|
56
|
+
VALIDATION_STEPS.each { |step| Rules.public_send(step, raw) }
|
|
43
57
|
end
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|