henitai 0.1.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 +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +182 -0
- data/assets/schema/henitai.schema.json +123 -0
- data/exe/henitai +6 -0
- data/lib/henitai/arid_node_filter.rb +97 -0
- data/lib/henitai/cli.rb +341 -0
- data/lib/henitai/configuration.rb +132 -0
- data/lib/henitai/configuration_validator.rb +293 -0
- data/lib/henitai/coverage_bootstrapper.rb +75 -0
- data/lib/henitai/coverage_formatter.rb +112 -0
- data/lib/henitai/equivalence_detector.rb +85 -0
- data/lib/henitai/execution_engine.rb +174 -0
- data/lib/henitai/git_diff_analyzer.rb +82 -0
- data/lib/henitai/integration.rb +417 -0
- data/lib/henitai/mutant/activator.rb +234 -0
- data/lib/henitai/mutant.rb +68 -0
- data/lib/henitai/mutant_generator.rb +158 -0
- data/lib/henitai/mutant_history_store.rb +279 -0
- data/lib/henitai/operator.rb +96 -0
- data/lib/henitai/operators/arithmetic_operator.rb +46 -0
- data/lib/henitai/operators/array_declaration.rb +52 -0
- data/lib/henitai/operators/assignment_expression.rb +78 -0
- data/lib/henitai/operators/block_statement.rb +31 -0
- data/lib/henitai/operators/boolean_literal.rb +70 -0
- data/lib/henitai/operators/conditional_expression.rb +184 -0
- data/lib/henitai/operators/equality_operator.rb +41 -0
- data/lib/henitai/operators/hash_literal.rb +66 -0
- data/lib/henitai/operators/logical_operator.rb +84 -0
- data/lib/henitai/operators/method_expression.rb +56 -0
- data/lib/henitai/operators/pattern_match.rb +66 -0
- data/lib/henitai/operators/range_literal.rb +40 -0
- data/lib/henitai/operators/return_value.rb +105 -0
- data/lib/henitai/operators/safe_navigation.rb +34 -0
- data/lib/henitai/operators/string_literal.rb +64 -0
- data/lib/henitai/operators.rb +25 -0
- data/lib/henitai/parser_current.rb +7 -0
- data/lib/henitai/reporter.rb +432 -0
- data/lib/henitai/result.rb +170 -0
- data/lib/henitai/runner.rb +183 -0
- data/lib/henitai/sampling_strategy.rb +33 -0
- data/lib/henitai/scenario_execution_result.rb +71 -0
- data/lib/henitai/source_parser.rb +41 -0
- data/lib/henitai/static_filter.rb +186 -0
- data/lib/henitai/stillborn_filter.rb +34 -0
- data/lib/henitai/subject.rb +71 -0
- data/lib/henitai/subject_resolver.rb +232 -0
- data/lib/henitai/syntax_validator.rb +16 -0
- data/lib/henitai/test_prioritizer.rb +55 -0
- data/lib/henitai/unparse_helper.rb +24 -0
- data/lib/henitai/version.rb +5 -0
- data/lib/henitai/warning_silencer.rb +16 -0
- data/lib/henitai.rb +51 -0
- data/sig/configuration_validator.rbs +29 -0
- data/sig/henitai.rbs +594 -0
- data/sig/unparser.rbs +3 -0
- metadata +153 -0
data/lib/henitai/cli.rb
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
module Henitai
|
|
5
|
+
# Command-line interface entry point.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# henitai run [options] [SUBJECT_PATTERN...]
|
|
9
|
+
#
|
|
10
|
+
# Options:
|
|
11
|
+
# --since GIT_REF Only mutate subjects changed since GIT_REF
|
|
12
|
+
# --use INTEGRATION Override integration from config (e.g. rspec)
|
|
13
|
+
# --config PATH Path to .henitai.yml (default: .henitai.yml)
|
|
14
|
+
# --operators SET Operator set: light (default) | full
|
|
15
|
+
# --jobs N Number of parallel workers (default: CPU count)
|
|
16
|
+
# --all-logs Print all captured child logs
|
|
17
|
+
# -h, --help Show this help message
|
|
18
|
+
# -v, --version Show version
|
|
19
|
+
# rubocop:disable Metrics/ClassLength
|
|
20
|
+
class CLI
|
|
21
|
+
INIT_TEMPLATE_LINES = [
|
|
22
|
+
"# yaml-language-server: $schema=./assets/schema/henitai.schema.json",
|
|
23
|
+
"includes:",
|
|
24
|
+
" - lib",
|
|
25
|
+
"mutation:",
|
|
26
|
+
" operators: light",
|
|
27
|
+
" timeout: 10.0",
|
|
28
|
+
" max_mutants_per_line: 1",
|
|
29
|
+
" max_flaky_retries: 3",
|
|
30
|
+
" sampling:",
|
|
31
|
+
" ratio: 0.05",
|
|
32
|
+
" strategy: stratified",
|
|
33
|
+
"reports_dir: reports",
|
|
34
|
+
"thresholds:",
|
|
35
|
+
" high: 80",
|
|
36
|
+
" low: 60"
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
OPERATOR_METADATA = {
|
|
40
|
+
"ArithmeticOperator" => ["Arithmetic operators", "a + b -> a - b"],
|
|
41
|
+
"EqualityOperator" => ["Comparison operators", "a == b -> a != b"],
|
|
42
|
+
"LogicalOperator" => ["Boolean operators", "a && b -> a || b"],
|
|
43
|
+
"BooleanLiteral" => ["Boolean literals", "true -> false"],
|
|
44
|
+
"ConditionalExpression" => ["Conditional branches", "if cond then ... end"],
|
|
45
|
+
"StringLiteral" => ["String literals", '"foo" -> ""'],
|
|
46
|
+
"ReturnValue" => ["Return expressions", "return x -> return nil"],
|
|
47
|
+
"ArrayDeclaration" => ["Array literals", "[1, 2] -> []"],
|
|
48
|
+
"HashLiteral" => ["Hash literals", "{ a: 1 } -> {}"],
|
|
49
|
+
"RangeLiteral" => ["Range literals", "1..5 -> 1...5"],
|
|
50
|
+
"SafeNavigation" => ["Safe navigation", "user&.name -> user.name"],
|
|
51
|
+
"PatternMatch" => ["Pattern matching", "in { x: Integer } -> in { x: String }"],
|
|
52
|
+
"BlockStatement" => ["Block statements", "{ do_work } -> {}"],
|
|
53
|
+
"MethodExpression" => ["Method calls", "call_service -> nil"],
|
|
54
|
+
"AssignmentExpression" => ["Assignment expressions", "x += 1 -> x -= 1"]
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
def self.start(argv)
|
|
58
|
+
new(argv).run
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def initialize(argv)
|
|
62
|
+
@argv = argv.dup
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def run
|
|
66
|
+
command = @argv.shift
|
|
67
|
+
case command
|
|
68
|
+
when "run" then run_command
|
|
69
|
+
when "version" then puts Henitai::VERSION
|
|
70
|
+
when "init" then init_command
|
|
71
|
+
when "operator" then operator_command
|
|
72
|
+
when nil, "-h", "--help" then puts help_text
|
|
73
|
+
else
|
|
74
|
+
warn "Unknown command: #{command}"
|
|
75
|
+
warn help_text
|
|
76
|
+
exit 1
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def run_command
|
|
83
|
+
@command_halted = false
|
|
84
|
+
options = parse_run_options
|
|
85
|
+
return if @command_halted
|
|
86
|
+
|
|
87
|
+
config = load_config(options)
|
|
88
|
+
result = run_pipeline(options, config)
|
|
89
|
+
exit(exit_status_for(result, config))
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
handle_run_error(e)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse_run_options
|
|
95
|
+
options = {}
|
|
96
|
+
build_run_option_parser(options).parse!(@argv)
|
|
97
|
+
options
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def configuration_overrides(options)
|
|
101
|
+
deep_compact(
|
|
102
|
+
{
|
|
103
|
+
integration: options[:integration],
|
|
104
|
+
all_logs: options[:all_logs],
|
|
105
|
+
mutation: {
|
|
106
|
+
operators: options[:operators],
|
|
107
|
+
timeout: options[:timeout]
|
|
108
|
+
},
|
|
109
|
+
jobs: options[:jobs]
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def deep_compact(value)
|
|
115
|
+
case value
|
|
116
|
+
when Hash
|
|
117
|
+
value.each_with_object({}) do |(key, nested_value), result|
|
|
118
|
+
compacted = deep_compact(nested_value)
|
|
119
|
+
result[key] = compacted unless compacted.nil?
|
|
120
|
+
end
|
|
121
|
+
when Array
|
|
122
|
+
value.map { |item| deep_compact(item) }.compact
|
|
123
|
+
else
|
|
124
|
+
value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_run_option_parser(options)
|
|
129
|
+
OptionParser.new do |opts|
|
|
130
|
+
opts.banner = "Usage: henitai run [options] [SUBJECT_PATTERN...]"
|
|
131
|
+
add_since_option(opts, options)
|
|
132
|
+
add_integration_option(opts, options)
|
|
133
|
+
add_config_option(opts, options)
|
|
134
|
+
add_operator_option(opts, options)
|
|
135
|
+
add_jobs_option(opts, options)
|
|
136
|
+
add_output_option(opts, options)
|
|
137
|
+
add_help_option(opts)
|
|
138
|
+
add_version_option(opts)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def add_since_option(opts, options)
|
|
143
|
+
opts.on("--since GIT_REF", "Only mutate subjects changed since GIT_REF") do |ref|
|
|
144
|
+
options[:since] = ref
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def add_integration_option(opts, options)
|
|
149
|
+
opts.on("--use INTEGRATION", "Test framework integration (rspec)") do |name|
|
|
150
|
+
options[:integration] = name
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def add_config_option(opts, options)
|
|
155
|
+
opts.on("--config PATH", "Path to .henitai.yml") do |path|
|
|
156
|
+
options[:config] = path
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def add_operator_option(opts, options)
|
|
161
|
+
opts.on("--operators SET", "Operator set: light | full") do |set|
|
|
162
|
+
options[:operators] = set
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def add_jobs_option(opts, options)
|
|
167
|
+
opts.on("--jobs N", Integer, "Number of parallel workers") do |n|
|
|
168
|
+
options[:jobs] = n
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def add_output_option(opts, options)
|
|
173
|
+
opts.on("--all-logs", "--verbose", "Print all captured child logs") do
|
|
174
|
+
options[:all_logs] = true
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def add_help_option(opts)
|
|
179
|
+
opts.on("-h", "--help", "Show this help") do
|
|
180
|
+
puts opts
|
|
181
|
+
@command_halted = true
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def add_version_option(opts)
|
|
186
|
+
opts.on("-v", "--version", "Show version") do
|
|
187
|
+
puts Henitai::VERSION
|
|
188
|
+
@command_halted = true
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def help_text
|
|
193
|
+
<<~HELP
|
|
194
|
+
Hen'i-tai 変異体 #{Henitai::VERSION} — Ruby 4 Mutation Testing
|
|
195
|
+
|
|
196
|
+
Usage:
|
|
197
|
+
henitai run [options] [SUBJECT_PATTERN...]
|
|
198
|
+
henitai version
|
|
199
|
+
henitai init [PATH]
|
|
200
|
+
henitai operator list
|
|
201
|
+
|
|
202
|
+
Examples:
|
|
203
|
+
bundle exec henitai run
|
|
204
|
+
bundle exec henitai run --since origin/main
|
|
205
|
+
bundle exec henitai run 'Foo::Bar#my_method'
|
|
206
|
+
bundle exec henitai run 'MyNamespace*' --operators full
|
|
207
|
+
bundle exec henitai init
|
|
208
|
+
bundle exec henitai operator list
|
|
209
|
+
|
|
210
|
+
Run `henitai run --help` for full option list.
|
|
211
|
+
HELP
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def run_pipeline(options, config)
|
|
215
|
+
runner = Runner.new(
|
|
216
|
+
config:,
|
|
217
|
+
subjects: subjects_from_argv,
|
|
218
|
+
since: options[:since]
|
|
219
|
+
)
|
|
220
|
+
runner.run
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def load_config(options)
|
|
224
|
+
Configuration.load(
|
|
225
|
+
path: options.fetch(:config, Configuration::CONFIG_FILE),
|
|
226
|
+
overrides: configuration_overrides(options)
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def subjects_from_argv
|
|
231
|
+
@argv.empty? ? nil : @argv.map { |expr| Subject.parse(expr) }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def handle_run_error(error)
|
|
235
|
+
warn "#{error.class}: #{error.message}"
|
|
236
|
+
exit 2
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def exit_status_for(result, config)
|
|
240
|
+
result.mutation_score.to_i >= config.thresholds.fetch(:low, 60) ? 0 : 1
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def init_command
|
|
244
|
+
path = @argv.shift || Configuration::CONFIG_FILE
|
|
245
|
+
unexpected_arguments = @argv.dup
|
|
246
|
+
warn "Unexpected arguments: #{unexpected_arguments.join(' ')}" unless unexpected_arguments.empty?
|
|
247
|
+
exit 1 unless unexpected_arguments.empty?
|
|
248
|
+
|
|
249
|
+
File.write(path, init_template)
|
|
250
|
+
puts "Created #{path}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def operator_command
|
|
254
|
+
subcommand = @argv.shift
|
|
255
|
+
case subcommand
|
|
256
|
+
when "list" then puts operator_list_text
|
|
257
|
+
when nil, "-h", "--help" then puts operator_help_text
|
|
258
|
+
else
|
|
259
|
+
warn "Unknown operator command: #{subcommand}"
|
|
260
|
+
warn operator_help_text
|
|
261
|
+
exit 1
|
|
262
|
+
end
|
|
263
|
+
rescue ArgumentError => e
|
|
264
|
+
warn e.message
|
|
265
|
+
exit 1
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def init_template
|
|
269
|
+
template = init_template_lines
|
|
270
|
+
template << integration_block if include_default_integration?
|
|
271
|
+
"#{template.join("\n")}\n"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def init_template_lines
|
|
275
|
+
INIT_TEMPLATE_LINES.dup
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def include_default_integration?
|
|
279
|
+
return true unless $stdin.tty?
|
|
280
|
+
|
|
281
|
+
print "Use the default RSpec integration? [Y/n] "
|
|
282
|
+
response = $stdin.gets&.strip&.downcase
|
|
283
|
+
response.nil? || response.empty? || !%w[n no].include?(response)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def integration_block
|
|
287
|
+
<<~YAML.chomp
|
|
288
|
+
integration:
|
|
289
|
+
name: rspec
|
|
290
|
+
YAML
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def operator_help_text
|
|
294
|
+
<<~HELP
|
|
295
|
+
Hen'i-tai operator commands
|
|
296
|
+
|
|
297
|
+
Usage:
|
|
298
|
+
henitai operator list
|
|
299
|
+
|
|
300
|
+
Run `henitai operator list` to see all built-in operators.
|
|
301
|
+
HELP
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def operator_list_text
|
|
305
|
+
validate_operator_metadata!
|
|
306
|
+
sections = [
|
|
307
|
+
operator_list_section("Light set", Operator::LIGHT_SET),
|
|
308
|
+
operator_list_section("Full set", Operator::FULL_SET)
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
["Available operators", *sections].join("\n")
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def operator_list_section(title, names)
|
|
315
|
+
rows = names.map { |name| operator_description_row(name) }
|
|
316
|
+
([title] + rows).join("\n")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def operator_description_row(name)
|
|
320
|
+
description, example = operator_metadata[name] || fallback_operator_metadata
|
|
321
|
+
|
|
322
|
+
format("- %<name>s: %<description>s (%<example>s)", name:, description:, example:)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def operator_metadata
|
|
326
|
+
OPERATOR_METADATA
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def fallback_operator_metadata
|
|
330
|
+
["No metadata available", "n/a"]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def validate_operator_metadata!
|
|
334
|
+
missing = Operator::FULL_SET - operator_metadata.keys
|
|
335
|
+
return if missing.empty?
|
|
336
|
+
|
|
337
|
+
raise ArgumentError, "Missing operator metadata for: #{missing.join(', ')}"
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
# rubocop:enable Metrics/ClassLength
|
|
341
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "psych"
|
|
4
|
+
|
|
5
|
+
require_relative "configuration_validator"
|
|
6
|
+
|
|
7
|
+
module Henitai
|
|
8
|
+
# Loads and validates .henitai.yml configuration.
|
|
9
|
+
#
|
|
10
|
+
# Configuration is resolved from built-in defaults and the project-root
|
|
11
|
+
# `.henitai.yml` file.
|
|
12
|
+
class Configuration
|
|
13
|
+
DEFAULT_TIMEOUT = 10.0
|
|
14
|
+
DEFAULT_OPERATORS = :light
|
|
15
|
+
DEFAULT_JOBS = nil # auto-detect
|
|
16
|
+
DEFAULT_MAX_MUTANTS_PER_LINE = 1
|
|
17
|
+
DEFAULT_MAX_FLAKY_RETRIES = 3
|
|
18
|
+
DEFAULT_REPORTS_DIR = "reports"
|
|
19
|
+
DEFAULT_COVERAGE_CRITERIA = {
|
|
20
|
+
test_result: true,
|
|
21
|
+
timeout: false,
|
|
22
|
+
process_abort: false
|
|
23
|
+
}.freeze
|
|
24
|
+
DEFAULT_THRESHOLDS = { high: 80, low: 60 }.freeze
|
|
25
|
+
CONFIG_FILE = ".henitai.yml"
|
|
26
|
+
|
|
27
|
+
attr_reader :integration, :includes, :operators, :timeout,
|
|
28
|
+
:ignore_patterns, :max_mutants_per_line, :sampling, :jobs,
|
|
29
|
+
:max_flaky_retries, :coverage_criteria, :thresholds,
|
|
30
|
+
:reporters, :reports_dir,
|
|
31
|
+
:dashboard, :all_logs
|
|
32
|
+
|
|
33
|
+
# @param path [String] path to .henitai.yml (default: project root)
|
|
34
|
+
def self.load(path: CONFIG_FILE, overrides: {})
|
|
35
|
+
new(path:, overrides:)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(path: CONFIG_FILE, overrides: {})
|
|
39
|
+
raw = load_raw_configuration(path)
|
|
40
|
+
unless raw.is_a?(Hash)
|
|
41
|
+
raise Henitai::ConfigurationError,
|
|
42
|
+
"Invalid configuration value for configuration: expected Hash, got #{raw.class}"
|
|
43
|
+
end
|
|
44
|
+
merged = merge_defaults(raw, symbolize_keys(overrides))
|
|
45
|
+
ConfigurationValidator.validate!(merged)
|
|
46
|
+
apply_defaults(merged)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def load_raw_configuration(path)
|
|
52
|
+
return {} unless File.exist?(path)
|
|
53
|
+
|
|
54
|
+
raw = Psych.safe_load(File.read(path), symbolize_names: true)
|
|
55
|
+
raw || {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def apply_defaults(raw)
|
|
59
|
+
apply_general_defaults(raw)
|
|
60
|
+
apply_mutation_defaults(raw)
|
|
61
|
+
apply_analysis_defaults(raw)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def apply_general_defaults(raw)
|
|
65
|
+
integration = raw[:integration]
|
|
66
|
+
@integration = if integration.is_a?(Hash)
|
|
67
|
+
integration[:name] || "rspec"
|
|
68
|
+
elsif integration.nil?
|
|
69
|
+
"rspec"
|
|
70
|
+
else
|
|
71
|
+
integration
|
|
72
|
+
end
|
|
73
|
+
@includes = raw[:includes] || ["lib"]
|
|
74
|
+
@jobs = raw[:jobs]
|
|
75
|
+
@reporters = raw[:reporters] || ["terminal"]
|
|
76
|
+
@reports_dir = raw[:reports_dir] || DEFAULT_REPORTS_DIR
|
|
77
|
+
@all_logs = raw[:all_logs] == true
|
|
78
|
+
# @type var empty_dashboard: Hash[Symbol, untyped]
|
|
79
|
+
empty_dashboard = {}
|
|
80
|
+
@dashboard = merge_defaults(empty_dashboard, raw[:dashboard])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def apply_mutation_defaults(raw)
|
|
84
|
+
mutation = raw[:mutation] || {}
|
|
85
|
+
|
|
86
|
+
@operators = (mutation[:operators] || DEFAULT_OPERATORS).to_sym
|
|
87
|
+
@timeout = mutation[:timeout] || DEFAULT_TIMEOUT
|
|
88
|
+
@ignore_patterns = mutation[:ignore_patterns] || []
|
|
89
|
+
@max_mutants_per_line = mutation[:max_mutants_per_line] || DEFAULT_MAX_MUTANTS_PER_LINE
|
|
90
|
+
@max_flaky_retries = if mutation.key?(:max_flaky_retries)
|
|
91
|
+
mutation[:max_flaky_retries]
|
|
92
|
+
else
|
|
93
|
+
DEFAULT_MAX_FLAKY_RETRIES
|
|
94
|
+
end
|
|
95
|
+
@sampling = mutation[:sampling]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def apply_analysis_defaults(raw)
|
|
99
|
+
@coverage_criteria = merge_defaults(DEFAULT_COVERAGE_CRITERIA,
|
|
100
|
+
raw[:coverage_criteria])
|
|
101
|
+
@thresholds = merge_defaults(DEFAULT_THRESHOLDS, raw[:thresholds])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def merge_defaults(defaults, overrides)
|
|
105
|
+
return defaults.dup if overrides.nil?
|
|
106
|
+
|
|
107
|
+
defaults.merge(overrides) do |_key, default_value, override_value|
|
|
108
|
+
if default_value.is_a?(Hash) && override_value.is_a?(Hash)
|
|
109
|
+
merge_defaults(default_value, override_value)
|
|
110
|
+
else
|
|
111
|
+
override_value
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def symbolize_keys(value)
|
|
117
|
+
case value
|
|
118
|
+
when Hash
|
|
119
|
+
# @type var result: Hash[Symbol, untyped]
|
|
120
|
+
result = {}
|
|
121
|
+
value.each do |key, val|
|
|
122
|
+
result[key.to_sym] = symbolize_keys(val)
|
|
123
|
+
end
|
|
124
|
+
result
|
|
125
|
+
when Array
|
|
126
|
+
value.map { |item| symbolize_keys(item) }
|
|
127
|
+
else
|
|
128
|
+
value
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|