henitai 0.2.1 → 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 +80 -1
- data/README.md +123 -2
- data/assets/schema/henitai.schema.json +35 -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 +14 -8
- data/lib/henitai/cli/command_support.rb +2 -1
- data/lib/henitai/cli/operator_command.rb +2 -1
- data/lib/henitai/cli/options.rb +21 -57
- data/lib/henitai/cli/run_command.rb +36 -3
- data/lib/henitai/cli/run_options.rb +108 -0
- data/lib/henitai/cli.rb +12 -4
- data/lib/henitai/composite_progress_reporter.rb +42 -0
- data/lib/henitai/configuration.rb +41 -9
- data/lib/henitai/configuration_validator/rules.rb +18 -0
- data/lib/henitai/configuration_validator/scalars.rb +46 -0
- data/lib/henitai/configuration_validator.rb +8 -1
- 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/execution_engine/env_scope.rb +67 -0
- data/lib/henitai/execution_engine.rb +111 -50
- data/lib/henitai/generated_artifacts.rb +58 -0
- data/lib/henitai/incremental_filter.rb +100 -0
- data/lib/henitai/integration/child_debug_support.rb +6 -2
- data/lib/henitai/integration/coverage_suppression.rb +9 -0
- data/lib/henitai/integration/minitest.rb +11 -43
- 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/rails_environment_preloader.rb +14 -0
- data/lib/henitai/integration/rspec_child_runner.rb +3 -3
- data/lib/henitai/integration/rspec_test_selection.rb +3 -0
- data/lib/henitai/integration/scenario_log_support.rb +64 -20
- data/lib/henitai/minitest_coverage_reporter.rb +4 -1
- data/lib/henitai/mutant/activator.rb +14 -0
- data/lib/henitai/mutant.rb +13 -6
- data/lib/henitai/mutant_history_store/sql.rb +21 -3
- data/lib/henitai/mutant_history_store/verdict_cache.rb +84 -0
- data/lib/henitai/mutant_history_store.rb +62 -12
- 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 +20 -5
- data/lib/henitai/per_test_coverage_selector.rb +11 -33
- data/lib/henitai/reporter/dashboard_metadata_provider.rb +113 -0
- data/lib/henitai/reporter.rb +238 -117
- data/lib/henitai/reports_directory_lock.rb +76 -0
- data/lib/henitai/result.rb +72 -9
- data/lib/henitai/runner.rb +82 -40
- data/lib/henitai/scenario_execution_result.rb +12 -0
- data/lib/henitai/slot_scheduler/draining.rb +21 -15
- data/lib/henitai/slot_scheduler.rb +73 -15
- data/lib/henitai/static_filter.rb +16 -6
- data/lib/henitai/survivor_rerun_strategy.rb +1 -1
- 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/verdict_fingerprint.rb +155 -0
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +14 -1
- data/sig/henitai.rbs +230 -25
- metadata +22 -3
- data/lib/henitai/parallel_execution_runner.rb +0 -153
|
@@ -24,7 +24,8 @@ module Henitai
|
|
|
24
24
|
"MethodChainUnwrap" => ["Method chain unwrap", "a.b.c -> a.b"],
|
|
25
25
|
"RegexMutator" => ["Regex literals", "/foo+/ -> /foo*/"],
|
|
26
26
|
"UnaryOperator" => ["Unary operators", "-x -> x"],
|
|
27
|
-
"UpdateOperator" => ["Compound assignment", "x += 1 -> x -= 1"]
|
|
27
|
+
"UpdateOperator" => ["Compound assignment", "x += 1 -> x -= 1"],
|
|
28
|
+
"EqualityIdentityOperator" => ["Identity-method comparisons", "a == b -> a.eql?(b)"]
|
|
28
29
|
}.freeze
|
|
29
30
|
|
|
30
31
|
private
|
data/lib/henitai/cli/options.rb
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
require_relative "run_options"
|
|
4
5
|
|
|
5
6
|
module Henitai
|
|
6
7
|
class CLI
|
|
7
8
|
# Builds the OptionParser instances for the `run` and `clean` commands and
|
|
8
9
|
# parses argv into an options Hash. Help/version options set
|
|
9
10
|
# +@command_halted+ so the caller can skip the rest of the command.
|
|
11
|
+
# Run-only option definitions live in {RunOptions}.
|
|
10
12
|
module Options
|
|
13
|
+
include RunOptions
|
|
14
|
+
|
|
11
15
|
private
|
|
12
16
|
|
|
13
17
|
def parse_run_options
|
|
@@ -25,19 +29,28 @@ module Henitai
|
|
|
25
29
|
def build_run_option_parser(options)
|
|
26
30
|
OptionParser.new do |opts|
|
|
27
31
|
opts.banner = "Usage: henitai run [options] [SUBJECT_PATTERN...]"
|
|
28
|
-
|
|
29
|
-
add_integration_option(opts, options)
|
|
30
|
-
add_config_option(opts, options)
|
|
31
|
-
add_operator_option(opts, options)
|
|
32
|
-
add_jobs_option(opts, options)
|
|
33
|
-
add_output_option(opts, options)
|
|
34
|
-
add_survivors_from_option(opts, options)
|
|
35
|
-
add_fail_on_survivors_option(opts, options)
|
|
32
|
+
add_run_flag_options(opts, options)
|
|
36
33
|
add_help_option(opts)
|
|
37
34
|
add_version_option(opts)
|
|
38
35
|
end
|
|
39
36
|
end
|
|
40
37
|
|
|
38
|
+
def add_run_flag_options(opts, options)
|
|
39
|
+
add_since_option(opts, options)
|
|
40
|
+
add_integration_option(opts, options)
|
|
41
|
+
add_config_option(opts, options)
|
|
42
|
+
add_operator_option(opts, options)
|
|
43
|
+
add_timeout_multiplier_option(opts, options)
|
|
44
|
+
add_jobs_option(opts, options)
|
|
45
|
+
add_output_option(opts, options)
|
|
46
|
+
add_survivors_from_option(opts, options)
|
|
47
|
+
add_incremental_option(opts, options)
|
|
48
|
+
add_force_option(opts, options)
|
|
49
|
+
add_dry_run_option(opts, options)
|
|
50
|
+
add_fail_on_survivors_option(opts, options)
|
|
51
|
+
add_strict_exit_codes_option(opts, options)
|
|
52
|
+
end
|
|
53
|
+
|
|
41
54
|
def build_clean_option_parser(options)
|
|
42
55
|
OptionParser.new do |opts|
|
|
43
56
|
opts.banner = "Usage: henitai clean [options]"
|
|
@@ -47,61 +60,12 @@ module Henitai
|
|
|
47
60
|
end
|
|
48
61
|
end
|
|
49
62
|
|
|
50
|
-
def add_since_option(opts, options)
|
|
51
|
-
opts.on("--since GIT_REF", "Only mutate subjects changed since GIT_REF") do |ref|
|
|
52
|
-
options[:since] = ref
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def add_integration_option(opts, options)
|
|
57
|
-
opts.on("--use INTEGRATION", "Test framework integration (rspec)") do |name|
|
|
58
|
-
options[:integration] = name
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
63
|
def add_config_option(opts, options)
|
|
63
64
|
opts.on("--config PATH", "Path to .henitai.yml") do |path|
|
|
64
65
|
options[:config] = path
|
|
65
66
|
end
|
|
66
67
|
end
|
|
67
68
|
|
|
68
|
-
def add_operator_option(opts, options)
|
|
69
|
-
opts.on("--operators SET", "Operator set: light | full") do |set|
|
|
70
|
-
options[:operators] = set
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def add_jobs_option(opts, options)
|
|
75
|
-
opts.on("--jobs N", Integer, "Number of parallel workers (default: 1)") do |n|
|
|
76
|
-
options[:jobs] = n
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def add_output_option(opts, options)
|
|
81
|
-
opts.on("--all-logs", "--verbose", "Print all captured child logs") do
|
|
82
|
-
options[:all_logs] = true
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def add_survivors_from_option(opts, options)
|
|
87
|
-
opts.on(
|
|
88
|
-
"--survivors-from PATH",
|
|
89
|
-
"Re-run only survivors from a prior report " \
|
|
90
|
-
"(partial rerun; threshold checks are skipped; dirty worktrees are included)"
|
|
91
|
-
) do |path|
|
|
92
|
-
options[:survivors_from] = path
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def add_fail_on_survivors_option(opts, options)
|
|
97
|
-
opts.on(
|
|
98
|
-
"--fail-on-survivors",
|
|
99
|
-
"Exit 1 for partial reruns when any survivors remain (otherwise exits 0)"
|
|
100
|
-
) do
|
|
101
|
-
options[:fail_on_survivors] = true
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
69
|
def add_help_option(opts)
|
|
106
70
|
opts.on("-h", "--help", "Show this help") do
|
|
107
71
|
puts opts
|
|
@@ -17,18 +17,34 @@ module Henitai
|
|
|
17
17
|
|
|
18
18
|
config = load_config(options)
|
|
19
19
|
result = run_pipeline(options, config)
|
|
20
|
-
exit(
|
|
20
|
+
exit(run_exit_status(result, config, options))
|
|
21
21
|
rescue StandardError => e
|
|
22
22
|
handle_run_error(e)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def run_exit_status(result, config, options)
|
|
26
|
+
# A dry run tests nothing, so there is no score to gate on.
|
|
27
|
+
return 0 if options[:dry_run]
|
|
28
|
+
|
|
29
|
+
exit_status_for(
|
|
30
|
+
result,
|
|
31
|
+
config,
|
|
32
|
+
fail_on_survivors: options[:fail_on_survivors],
|
|
33
|
+
strict_exit_codes: options[:strict_exit_codes]
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
25
37
|
def run_pipeline(options, config)
|
|
26
38
|
resolved_survivors_from = resolve_survivors_from(options[:survivors_from])
|
|
27
39
|
runner = Runner.new(
|
|
28
40
|
config:,
|
|
29
41
|
subjects: subjects_from_argv,
|
|
30
42
|
since: options[:since],
|
|
31
|
-
survivors_from: resolved_survivors_from
|
|
43
|
+
survivors_from: resolved_survivors_from,
|
|
44
|
+
mode: {
|
|
45
|
+
dry_run: options.fetch(:dry_run, false),
|
|
46
|
+
incremental: options.fetch(:incremental, false) && !options.fetch(:force, false)
|
|
47
|
+
}
|
|
32
48
|
)
|
|
33
49
|
runner.run
|
|
34
50
|
end
|
|
@@ -76,7 +92,7 @@ module Henitai
|
|
|
76
92
|
nil
|
|
77
93
|
end
|
|
78
94
|
|
|
79
|
-
def exit_status_for(result, config, fail_on_survivors: false)
|
|
95
|
+
def exit_status_for(result, config, fail_on_survivors: false, strict_exit_codes: false)
|
|
80
96
|
if result.respond_to?(:partial_rerun?) && result.partial_rerun?
|
|
81
97
|
warn "henitai: partial rerun - mutation score threshold not evaluated"
|
|
82
98
|
return result.survived.positive? ? 1 : 0 if fail_on_survivors
|
|
@@ -84,6 +100,23 @@ module Henitai
|
|
|
84
100
|
return 0
|
|
85
101
|
end
|
|
86
102
|
|
|
103
|
+
strict_status = strict_exit_codes ? strict_status_for(result) : nil
|
|
104
|
+
strict_status || threshold_status_for(result, config)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Expanded, opt-in exit codes (precedence: timeout > runtime/compile
|
|
108
|
+
# error > threshold miss). The timeout code is informational and
|
|
109
|
+
# independent of coverage_criteria.timeout: a run can pass its threshold
|
|
110
|
+
# and still exit 3.
|
|
111
|
+
def strict_status_for(result)
|
|
112
|
+
statuses = result.mutants.map(&:status)
|
|
113
|
+
return 3 if statuses.include?(:timeout)
|
|
114
|
+
return 4 if statuses.include?(:runtime_error) || statuses.include?(:compile_error)
|
|
115
|
+
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def threshold_status_for(result, config)
|
|
87
120
|
score = result.mutation_score
|
|
88
121
|
# No valid mutants to evaluate (e.g. an incremental run with no changed
|
|
89
122
|
# code) cannot fail a threshold — treat it as success.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
class CLI
|
|
5
|
+
# Option definitions specific to `henitai run`, mixed into {Options} to
|
|
6
|
+
# keep each module focused: {Options} owns parser construction and the
|
|
7
|
+
# shared options, this module owns the run-only flags.
|
|
8
|
+
module RunOptions
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def add_since_option(opts, options)
|
|
12
|
+
opts.on("--since GIT_REF", "Only mutate subjects changed since GIT_REF") do |ref|
|
|
13
|
+
options[:since] = ref
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_integration_option(opts, options)
|
|
18
|
+
opts.on("--use INTEGRATION", "Test framework integration (rspec)") do |name|
|
|
19
|
+
options[:integration] = name
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_operator_option(opts, options)
|
|
24
|
+
opts.on("--operators SET", "Operator set: light | full") do |set|
|
|
25
|
+
options[:operators] = set
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_timeout_multiplier_option(opts, options)
|
|
30
|
+
opts.on(
|
|
31
|
+
"--timeout-multiplier N", Float,
|
|
32
|
+
"Multiplier applied to the measured per-mutant test baseline " \
|
|
33
|
+
"when mutation.timeout is unset (default: 3.0)"
|
|
34
|
+
) do |n|
|
|
35
|
+
options[:timeout_multiplier] = n
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_jobs_option(opts, options)
|
|
40
|
+
opts.on("--jobs N", Integer, "Number of parallel workers (default: 1)") do |n|
|
|
41
|
+
options[:jobs] = n
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def add_output_option(opts, options)
|
|
46
|
+
opts.on("--all-logs", "--verbose", "Print all captured child logs") do
|
|
47
|
+
options[:all_logs] = true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_survivors_from_option(opts, options)
|
|
52
|
+
opts.on(
|
|
53
|
+
"--survivors-from PATH",
|
|
54
|
+
"Re-run only survivors from a prior report " \
|
|
55
|
+
"(partial rerun; threshold checks are skipped; dirty worktrees are included)"
|
|
56
|
+
) do |path|
|
|
57
|
+
options[:survivors_from] = path
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_dry_run_option(opts, options)
|
|
62
|
+
opts.on(
|
|
63
|
+
"--dry-run",
|
|
64
|
+
"List the post-filter mutant set without executing mutants (always exits 0)"
|
|
65
|
+
) do
|
|
66
|
+
options[:dry_run] = true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def add_incremental_option(opts, options)
|
|
71
|
+
opts.on(
|
|
72
|
+
"--incremental",
|
|
73
|
+
"Reuse still-valid Killed verdicts from the history store instead of re-executing them"
|
|
74
|
+
) do
|
|
75
|
+
options[:incremental] = true
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_force_option(opts, options)
|
|
80
|
+
opts.on(
|
|
81
|
+
"--force",
|
|
82
|
+
"Bypass verdict reuse and execute every mutant (only meaningful with --incremental)"
|
|
83
|
+
) do
|
|
84
|
+
options[:force] = true
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_fail_on_survivors_option(opts, options)
|
|
89
|
+
opts.on(
|
|
90
|
+
"--fail-on-survivors",
|
|
91
|
+
"Exit 1 for partial reruns when any survivors remain (otherwise exits 0)"
|
|
92
|
+
) do
|
|
93
|
+
options[:fail_on_survivors] = true
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def add_strict_exit_codes_option(opts, options)
|
|
98
|
+
opts.on(
|
|
99
|
+
"--strict-exit-codes",
|
|
100
|
+
"Expanded exit codes: 0 threshold met, 1 threshold miss, " \
|
|
101
|
+
"2 framework error, 3 timeouts present, 4 runtime/compile errors present"
|
|
102
|
+
) do
|
|
103
|
+
options[:strict_exit_codes] = true
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/henitai/cli.rb
CHANGED
|
@@ -34,12 +34,20 @@ module Henitai
|
|
|
34
34
|
include OperatorCommand
|
|
35
35
|
|
|
36
36
|
REPORT_CLEANUP_PATHS = [
|
|
37
|
-
%w[mutation-logs baseline.log],
|
|
38
|
-
%w[mutation-logs baseline.stdout.log],
|
|
39
|
-
%w[mutation-logs baseline.stderr.log],
|
|
40
37
|
%w[coverage .resultset.json],
|
|
41
38
|
%w[coverage .last_run.json],
|
|
42
|
-
["henitai_per_test.json"]
|
|
39
|
+
["henitai_per_test.json"],
|
|
40
|
+
[CoverageBootstrapper::DEPENDENCY_MANIFEST_FILE]
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# Whole directories of per-mutant scratch artifacts, removed recursively.
|
|
44
|
+
# `mutation-logs` accumulates one stdout/stderr/combined log trio per
|
|
45
|
+
# mutant (and baseline) and `mutation-coverage` one dir per mutant, so both
|
|
46
|
+
# grow to GBs on a large run; deleting the tree is the only way to reclaim
|
|
47
|
+
# it (rm_f cannot remove a directory).
|
|
48
|
+
REPORT_CLEANUP_DIRS = [
|
|
49
|
+
%w[mutation-logs],
|
|
50
|
+
%w[mutation-coverage]
|
|
43
51
|
].freeze
|
|
44
52
|
|
|
45
53
|
def self.start(argv)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
# Fans a single #progress callback out to several observers so the execution
|
|
5
|
+
# engines keep exactly one progress hook. Used to run the terminal reporter
|
|
6
|
+
# and the checkpoint writer side by side.
|
|
7
|
+
class CompositeProgressReporter
|
|
8
|
+
# Assembles the active progress reporters for a run: the terminal reporter
|
|
9
|
+
# when terminal output is on, and the checkpoint writer when enabled and a
|
|
10
|
+
# file report (json/html) is configured. Returns a single reporter when only
|
|
11
|
+
# one is active, the composite when several are, or nil when none are.
|
|
12
|
+
def self.for(config:, source_provider:, full_run:)
|
|
13
|
+
active = [terminal_for(config), checkpoint_for(config, source_provider, full_run)].compact
|
|
14
|
+
return active.first if active.one?
|
|
15
|
+
|
|
16
|
+
active.empty? ? nil : new(active)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.terminal_for(config)
|
|
20
|
+
Reporter::Terminal.new(config:) if reporter?(config, "terminal")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.checkpoint_for(config, source_provider, full_run)
|
|
24
|
+
return unless config.respond_to?(:checkpoint_enabled) && config.checkpoint_enabled
|
|
25
|
+
return unless reporter?(config, "json") || reporter?(config, "html")
|
|
26
|
+
|
|
27
|
+
CheckpointReporter.new(config:, source_provider:, authoritative: full_run)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.reporter?(config, name)
|
|
31
|
+
Array(config.reporters).map(&:to_s).include?(name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(reporters)
|
|
35
|
+
@reporters = reporters.compact
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def progress(mutant, scenario_result: nil)
|
|
39
|
+
@reporters.each { |reporter| reporter.progress(mutant, scenario_result:) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -10,10 +10,21 @@ module Henitai
|
|
|
10
10
|
# Configuration is resolved from built-in defaults and the project-root
|
|
11
11
|
# `.henitai.yml` file.
|
|
12
12
|
class Configuration
|
|
13
|
-
DEFAULT_TIMEOUT
|
|
13
|
+
DEFAULT_TIMEOUT = 10.0
|
|
14
|
+
DEFAULT_TIMEOUT_MULTIPLIER = 3.0
|
|
14
15
|
DEFAULT_OPERATORS = :light
|
|
15
16
|
DEFAULT_JOBS = 1
|
|
16
17
|
DEFAULT_MAX_FLAKY_RETRIES = 3
|
|
18
|
+
# Cap on captured child stdout/stderr per stream (bytes). A runaway mutant
|
|
19
|
+
# (e.g. one that puts henitai's own suite into a spewing recursion) can
|
|
20
|
+
# otherwise write hundreds of MB per child; overflow is discarded.
|
|
21
|
+
DEFAULT_MAX_LOG_BYTES = 5_000_000
|
|
22
|
+
# Ceiling for the per-mutant auto-calibrated timeout (seconds). Keeps a
|
|
23
|
+
# runaway from running for minutes when the calibrated value is large.
|
|
24
|
+
DEFAULT_MAX_TIMEOUT = 30.0
|
|
25
|
+
# Incremental report checkpoint cadence for long full runs.
|
|
26
|
+
DEFAULT_CHECKPOINT_EVERY = 200
|
|
27
|
+
DEFAULT_CHECKPOINT_INTERVAL = 30.0
|
|
17
28
|
DEFAULT_REPORTS_DIR = "reports"
|
|
18
29
|
DEFAULT_COVERAGE_CRITERIA = {
|
|
19
30
|
test_result: true,
|
|
@@ -23,12 +34,20 @@ module Henitai
|
|
|
23
34
|
DEFAULT_THRESHOLDS = { high: 80, low: 60 }.freeze
|
|
24
35
|
CONFIG_FILE = ".henitai.yml"
|
|
25
36
|
|
|
26
|
-
attr_reader :integration, :includes, :excludes, :operators, :timeout,
|
|
27
|
-
:ignore_patterns, :sampling, :jobs,
|
|
28
|
-
:max_flaky_retries, :
|
|
37
|
+
attr_reader :integration, :includes, :excludes, :test_excludes, :operators, :timeout,
|
|
38
|
+
:timeout_multiplier, :ignore_patterns, :sampling, :jobs,
|
|
39
|
+
:max_flaky_retries, :max_log_bytes, :max_timeout,
|
|
40
|
+
:coverage_criteria, :thresholds,
|
|
29
41
|
:reporters, :reports_dir,
|
|
42
|
+
:checkpoint_enabled, :checkpoint_every, :checkpoint_interval,
|
|
30
43
|
:dashboard, :all_logs
|
|
31
44
|
|
|
45
|
+
# True when mutation.timeout was set explicitly (file or CLI override);
|
|
46
|
+
# a fixed timeout disables per-mutant auto-calibration.
|
|
47
|
+
def timeout_configured?
|
|
48
|
+
@timeout_configured
|
|
49
|
+
end
|
|
50
|
+
|
|
32
51
|
# @param path [String] path to .henitai.yml (default: project root)
|
|
33
52
|
def self.load(path: CONFIG_FILE, overrides: {})
|
|
34
53
|
new(path:, overrides:)
|
|
@@ -66,6 +85,7 @@ module Henitai
|
|
|
66
85
|
def apply_defaults(raw)
|
|
67
86
|
apply_general_defaults(raw)
|
|
68
87
|
apply_mutation_defaults(raw)
|
|
88
|
+
apply_reports_defaults(raw)
|
|
69
89
|
apply_analysis_defaults(raw)
|
|
70
90
|
end
|
|
71
91
|
|
|
@@ -73,6 +93,7 @@ module Henitai
|
|
|
73
93
|
@integration = resolve_integration_default(raw[:integration])
|
|
74
94
|
@includes = raw[:includes] || ["lib"]
|
|
75
95
|
@excludes = raw[:excludes] || []
|
|
96
|
+
@test_excludes = raw[:test_excludes] || []
|
|
76
97
|
@jobs = raw.fetch(:jobs, DEFAULT_JOBS)
|
|
77
98
|
@reporters = raw[:reporters] || ["terminal"]
|
|
78
99
|
@reports_dir = raw[:reports_dir] || DEFAULT_REPORTS_DIR
|
|
@@ -84,14 +105,25 @@ module Henitai
|
|
|
84
105
|
mutation = raw[:mutation] || {}
|
|
85
106
|
|
|
86
107
|
@operators = (mutation[:operators] || DEFAULT_OPERATORS).to_sym
|
|
108
|
+
@timeout_configured = !mutation[:timeout].nil?
|
|
87
109
|
@timeout = mutation[:timeout] || DEFAULT_TIMEOUT
|
|
110
|
+
@timeout_multiplier = mutation[:timeout_multiplier] || DEFAULT_TIMEOUT_MULTIPLIER
|
|
88
111
|
@ignore_patterns = mutation[:ignore_patterns] || []
|
|
89
|
-
@max_flaky_retries = if mutation.key?(:max_flaky_retries)
|
|
90
|
-
mutation[:max_flaky_retries]
|
|
91
|
-
else
|
|
92
|
-
DEFAULT_MAX_FLAKY_RETRIES
|
|
93
|
-
end
|
|
94
112
|
@sampling = mutation[:sampling]
|
|
113
|
+
apply_mutation_limit_defaults(mutation)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def apply_mutation_limit_defaults(mutation)
|
|
117
|
+
@max_flaky_retries = mutation.fetch(:max_flaky_retries, DEFAULT_MAX_FLAKY_RETRIES)
|
|
118
|
+
@max_log_bytes = mutation[:max_log_bytes] || DEFAULT_MAX_LOG_BYTES
|
|
119
|
+
@max_timeout = mutation[:max_timeout] || DEFAULT_MAX_TIMEOUT
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def apply_reports_defaults(raw)
|
|
123
|
+
reports = raw[:reports] || {}
|
|
124
|
+
@checkpoint_enabled = reports.fetch(:checkpoint, true)
|
|
125
|
+
@checkpoint_every = reports[:checkpoint_every] || DEFAULT_CHECKPOINT_EVERY
|
|
126
|
+
@checkpoint_interval = reports[:checkpoint_interval] || DEFAULT_CHECKPOINT_INTERVAL
|
|
95
127
|
end
|
|
96
128
|
|
|
97
129
|
def apply_analysis_defaults(raw)
|
|
@@ -34,6 +34,10 @@ module Henitai
|
|
|
34
34
|
Scalars.validate_string_array(raw[:excludes], "excludes")
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def validate_test_excludes(raw)
|
|
38
|
+
Scalars.validate_string_array(raw[:test_excludes], "test_excludes")
|
|
39
|
+
end
|
|
40
|
+
|
|
37
41
|
def validate_jobs(raw)
|
|
38
42
|
value = raw[:jobs]
|
|
39
43
|
return if value.nil?
|
|
@@ -50,6 +54,17 @@ module Henitai
|
|
|
50
54
|
Scalars.validate_optional_string(raw[:reports_dir], "reports_dir")
|
|
51
55
|
end
|
|
52
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
|
+
|
|
53
68
|
def validate_all_logs(raw)
|
|
54
69
|
value = raw[:all_logs]
|
|
55
70
|
return if value.nil?
|
|
@@ -81,7 +96,10 @@ module Henitai
|
|
|
81
96
|
|
|
82
97
|
def validate_mutation_limits(value)
|
|
83
98
|
Scalars.validate_timeout(value[:timeout])
|
|
99
|
+
Scalars.validate_timeout_multiplier(value[:timeout_multiplier])
|
|
84
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])
|
|
85
103
|
end
|
|
86
104
|
|
|
87
105
|
def validate_mutation_filters(value)
|
|
@@ -30,6 +30,16 @@ module Henitai
|
|
|
30
30
|
)
|
|
31
31
|
end
|
|
32
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
|
+
|
|
33
43
|
def validate_threshold(value, path)
|
|
34
44
|
return if value.is_a?(Integer) && value.between?(0, 100)
|
|
35
45
|
|
|
@@ -83,6 +93,42 @@ module Henitai
|
|
|
83
93
|
)
|
|
84
94
|
end
|
|
85
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
|
+
|
|
86
132
|
def validate_sampling_ratio(value)
|
|
87
133
|
return if value.nil?
|
|
88
134
|
return if value.is_a?(Numeric) && value >= 0.0 && value <= 1.0
|
|
@@ -13,16 +13,21 @@ module Henitai
|
|
|
13
13
|
integration
|
|
14
14
|
includes
|
|
15
15
|
excludes
|
|
16
|
+
test_excludes
|
|
16
17
|
mutation
|
|
17
18
|
coverage_criteria
|
|
18
19
|
thresholds
|
|
19
20
|
reporters
|
|
20
21
|
reports_dir
|
|
22
|
+
reports
|
|
21
23
|
all_logs
|
|
22
24
|
dashboard
|
|
23
25
|
jobs
|
|
24
26
|
].freeze
|
|
25
|
-
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
|
|
26
31
|
VALID_SAMPLING_KEYS = %i[ratio strategy].freeze
|
|
27
32
|
VALID_COVERAGE_CRITERIA_KEYS = %i[test_result timeout process_abort].freeze
|
|
28
33
|
VALID_THRESHOLDS_KEYS = %i[high low].freeze
|
|
@@ -34,9 +39,11 @@ module Henitai
|
|
|
34
39
|
validate_integration
|
|
35
40
|
validate_includes
|
|
36
41
|
validate_excludes
|
|
42
|
+
validate_test_excludes
|
|
37
43
|
validate_jobs
|
|
38
44
|
validate_reporters
|
|
39
45
|
validate_reports_dir
|
|
46
|
+
validate_reports
|
|
40
47
|
validate_all_logs
|
|
41
48
|
validate_dashboard
|
|
42
49
|
validate_mutation
|