saboteur 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +115 -0
- data/README.md +34 -6
- data/lib/saboteur/attribute_finder.rb +113 -0
- data/lib/saboteur/cli/presets.rb +41 -1
- data/lib/saboteur/cli/run_budget_options.rb +3 -1
- data/lib/saboteur/cli/source_set.rb +0 -18
- data/lib/saboteur/cli.rb +4 -2
- data/lib/saboteur/config.rb +4 -1
- data/lib/saboteur/config_file.rb +12 -8
- data/lib/saboteur/custom_map.rb +12 -1
- data/lib/saboteur/dirty_tree_warning.rb +5 -3
- data/lib/saboteur/doctor.rb +4 -0
- data/lib/saboteur/engine/baseline_guard.rb +76 -10
- data/lib/saboteur/engine/boot.rb +7 -3
- data/lib/saboteur/engine/bytecode_equivalence.rb +17 -1
- data/lib/saboteur/engine/candidate_selection.rb +1 -0
- data/lib/saboteur/engine/coverage_store.rb +25 -3
- data/lib/saboteur/engine/diagnostics.rb +1 -1
- data/lib/saboteur/engine/gated_run.rb +3 -1
- data/lib/saboteur/engine/load_coverage.rb +18 -0
- data/lib/saboteur/engine/pipelines.rb +8 -0
- data/lib/saboteur/engine/run_budget.rb +5 -2
- data/lib/saboteur/engine/sandbox_policy.rb +21 -3
- data/lib/saboteur/engine/scheduler.rb +32 -24
- data/lib/saboteur/engine/selection_announcement.rb +8 -3
- data/lib/saboteur/engine/subject_selection.rb +115 -31
- data/lib/saboteur/engine/test_guess.rb +106 -16
- data/lib/saboteur/engine.rb +3 -2
- data/lib/saboteur/execution/external_runner.rb +18 -0
- data/lib/saboteur/execution/fiddle_loader.rb +34 -0
- data/lib/saboteur/execution/marker_sweep.rb +2 -1
- data/lib/saboteur/execution/process_table.rb +7 -3
- data/lib/saboteur/execution/reevaluator.rb +7 -0
- data/lib/saboteur/execution/sandbox.rb +10 -4
- data/lib/saboteur/execution/seatbelt.rb +10 -3
- data/lib/saboteur/execution/supervisor.rb +2 -1
- data/lib/saboteur/execution/test_command_shape.rb +149 -0
- data/lib/saboteur/frameworks/sqlite_memory_database.rb +32 -2
- data/lib/saboteur/integration/rspec.rb +99 -11
- data/lib/saboteur/mutation_source/argument_swap.rb +54 -0
- data/lib/saboteur/mutation_source/builtin.rb +3 -1
- data/lib/saboteur/mutation_source/hazard_guard.rb +94 -13
- data/lib/saboteur/mutation_source/operator_examples.rb +1 -0
- data/lib/saboteur/mutation_source/operator_registry.rb +1 -0
- data/lib/saboteur/preview.rb +5 -0
- data/lib/saboteur/reporter/agent_incomplete.rb +20 -5
- data/lib/saboteur/reporter/terminal.rb +16 -1
- data/lib/saboteur/selection/coverage_map.rb +17 -2
- data/lib/saboteur/version.rb +1 -1
- data/lib/saboteur.rb +1 -0
- metadata +5 -15
|
@@ -16,12 +16,15 @@ module Saboteur
|
|
|
16
16
|
reasons = []
|
|
17
17
|
shard_reason(reasons, report)
|
|
18
18
|
add(reasons, "no_coverage", skipped_for_coverage(report), "subjects or mutants had no covering test")
|
|
19
|
-
load_only = report.counts[:ran_at_load] + report.skipped_subjects.values.count do |reason|
|
|
20
|
-
reason == Selection::CoverageMap::RAN_AT_LOAD
|
|
21
|
-
end
|
|
22
19
|
add(reasons, "unsupported", unsupported_subjects(report),
|
|
23
20
|
"subjects were skipped as unsupported (for example defined inside a block); no mutant was judged")
|
|
24
|
-
add(reasons, "ran_at_load",
|
|
21
|
+
add(reasons, "ran_at_load", load_only_subjects(report), "code ran only at load; no mutant was judged")
|
|
22
|
+
# A file loaded before Coverage.start has no attribution at all:
|
|
23
|
+
# not unsupported (the shape is fine) and not ran_at_load (the
|
|
24
|
+
# load-only evidence does not exist) - its own code (#688).
|
|
25
|
+
add(reasons, "preloaded", preloaded_subjects(report),
|
|
26
|
+
"files loaded before coverage started; their subjects could not be attributed to tests; " \
|
|
27
|
+
"no mutant was judged")
|
|
25
28
|
add(reasons, "unreachable", report.counts[:unreachable], "tests did not call the replacement definition")
|
|
26
29
|
add(reasons, "mutant_cap", omitted_by_cap(report), "mutants omitted by max_mutants_per_method")
|
|
27
30
|
add(reasons, "errors", report.counts[:error], "mutants errored; their verdict did not hold up")
|
|
@@ -80,9 +83,21 @@ module Saboteur
|
|
|
80
83
|
report.skipped_subjects.values.count(NO_COVERAGE) + report.counts[:skipped]
|
|
81
84
|
end
|
|
82
85
|
|
|
86
|
+
# Load-only mutants plus the subjects skipped for the same reason.
|
|
87
|
+
def load_only_subjects(report)
|
|
88
|
+
report.counts[:ran_at_load] + report.skipped_subjects.values.count do |reason|
|
|
89
|
+
reason == Selection::CoverageMap::RAN_AT_LOAD
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def preloaded_subjects(report)
|
|
94
|
+
report.skipped_subjects.values.count(Selection::CoverageMap::PRELOADED)
|
|
95
|
+
end
|
|
96
|
+
|
|
83
97
|
def unsupported_subjects(report)
|
|
84
98
|
report.skipped_subjects.values.count do |reason|
|
|
85
|
-
reason != NO_COVERAGE && reason != Selection::CoverageMap::RAN_AT_LOAD
|
|
99
|
+
reason != NO_COVERAGE && reason != Selection::CoverageMap::RAN_AT_LOAD &&
|
|
100
|
+
reason != Selection::CoverageMap::PRELOADED
|
|
86
101
|
end
|
|
87
102
|
end
|
|
88
103
|
|
|
@@ -87,9 +87,18 @@ module Saboteur
|
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# An early stop runs fewer mutants than the selection: say both
|
|
91
|
+
# numbers so "4 mutants" is not read as the whole plan (#582).
|
|
92
|
+
def mutant_counts(report)
|
|
93
|
+
planned = report.early_stop&.planned_mutants
|
|
94
|
+
return "#{report.results.size} mutants" unless planned && planned > report.results.size
|
|
95
|
+
|
|
96
|
+
"#{planned} mutants selected, #{report.results.size} attempted"
|
|
97
|
+
end
|
|
98
|
+
|
|
90
99
|
def summary(report)
|
|
91
100
|
seed = report.seed && ", seed #{report.seed} (repeat with --seed #{report.seed})"
|
|
92
|
-
@out.puts "saboteur #{VERSION}: #{report.subjects.size} subjects, #{report
|
|
101
|
+
@out.puts "saboteur #{VERSION}: #{report.subjects.size} subjects, #{mutant_counts(report)}, " \
|
|
93
102
|
"wall #{format('%.2f', report.wall_time)} s#{seed}"
|
|
94
103
|
@out.puts report.shard.line if report.shard
|
|
95
104
|
@out.puts status_line(report)
|
|
@@ -258,6 +267,12 @@ module Saboteur
|
|
|
258
267
|
def budget_shortfall(stop, completed)
|
|
259
268
|
return unless stop.reason == "max_duration"
|
|
260
269
|
|
|
270
|
+
# Exit 4 alone does not say whether anything was judged: headline
|
|
271
|
+
# it when the baselines and coverage map ate the whole budget (#637).
|
|
272
|
+
if completed.zero?
|
|
273
|
+
@out.puts @palette.paint(:yellow, "no mutants ran before the --max-duration budget was spent; " \
|
|
274
|
+
"there is no score to judge")
|
|
275
|
+
end
|
|
261
276
|
line = if stop.planned_mutants
|
|
262
277
|
"partial: #{stop.planned_mutants - completed} of #{stop.planned_mutants} mutants not run"
|
|
263
278
|
else
|
|
@@ -20,9 +20,10 @@ module Saboteur
|
|
|
20
20
|
FORMAT = 11
|
|
21
21
|
|
|
22
22
|
RAN_AT_LOAD = "ran at load: the code ran while the app loaded; a forked test cannot observe a change here"
|
|
23
|
+
PRELOADED = "loaded before coverage started"
|
|
23
24
|
|
|
24
25
|
attr_reader :lines, :tests, :build_seconds, :loaded_files, :durations, :methods, :load_lines, :load_methods,
|
|
25
|
-
:branches
|
|
26
|
+
:branches, :preloaded
|
|
26
27
|
|
|
27
28
|
# lines: { "/abs/file.rb" => { 12 => ["id1", "id2"] } }
|
|
28
29
|
# methods: { "/abs/file.rb" => { 12 => ["id1", "id2"] } } keyed by the
|
|
@@ -30,7 +31,7 @@ module Saboteur
|
|
|
30
31
|
# so methods carry those subjects (issue #107).
|
|
31
32
|
# durations: { "id1" => seconds }
|
|
32
33
|
def initialize(lines:, tests:, build_seconds: nil, durations: {}, methods: {}, load_lines: {}, load_methods: {},
|
|
33
|
-
branches: {}, loaded_files: nil)
|
|
34
|
+
branches: {}, loaded_files: nil, preloaded: [])
|
|
34
35
|
@lines = lines
|
|
35
36
|
@tests = tests
|
|
36
37
|
@build_seconds = build_seconds
|
|
@@ -40,6 +41,7 @@ module Saboteur
|
|
|
40
41
|
@load_methods = load_methods
|
|
41
42
|
@branches = branches
|
|
42
43
|
@loaded_files = loaded_files || []
|
|
44
|
+
@preloaded = preloaded.to_set { |file| CoverageMap.canonical(file) }
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
# Source files the given tests touched, sorted and deduped: the
|
|
@@ -99,6 +101,19 @@ module Saboteur
|
|
|
99
101
|
loaded_files: loaded_files)
|
|
100
102
|
end
|
|
101
103
|
|
|
104
|
+
# Files that loaded before Coverage.start (gemspec require, RUBYOPT
|
|
105
|
+
# -r): they can never carry hits, so their subjects are reported as
|
|
106
|
+
# loaded before coverage started instead of no coverage.
|
|
107
|
+
def with_preloaded(paths)
|
|
108
|
+
self.class.new(lines: lines, tests: tests, build_seconds: build_seconds, durations: durations,
|
|
109
|
+
methods: methods, load_lines: load_lines, load_methods: load_methods, branches: branches,
|
|
110
|
+
loaded_files: loaded_files, preloaded: paths)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def preloaded?(file)
|
|
114
|
+
preloaded.include?(CoverageMap.canonical(file))
|
|
115
|
+
end
|
|
116
|
+
|
|
102
117
|
# A def the boot called (a def-time macro's class-body caller, say)
|
|
103
118
|
# and no test or hook called again: re-evaluating it post-boot
|
|
104
119
|
# changes nothing a selected test can observe, because closure
|
data/lib/saboteur/version.rb
CHANGED
data/lib/saboteur.rb
CHANGED
|
@@ -51,6 +51,7 @@ require_relative "saboteur/integration/base"
|
|
|
51
51
|
require_relative "saboteur/subject"
|
|
52
52
|
require_relative "saboteur/refinement_activations"
|
|
53
53
|
require_relative "saboteur/subject_finder"
|
|
54
|
+
require_relative "saboteur/attribute_finder"
|
|
54
55
|
require_relative "saboteur/constant_subject"
|
|
55
56
|
require_relative "saboteur/constant_subject_finder"
|
|
56
57
|
require_relative "saboteur/constant_reads"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: saboteur
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vamsi Pavan Mahesh Gunturu
|
|
@@ -10,20 +10,6 @@ bindir: exe
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-10-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: fiddle
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - ">="
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '1.1'
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ">="
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '1.1'
|
|
27
13
|
- !ruby/object:Gem::Dependency
|
|
28
14
|
name: prism
|
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -61,6 +47,7 @@ files:
|
|
|
61
47
|
- lib/generators/saboteur/install/install_generator.rb
|
|
62
48
|
- lib/saboteur.rb
|
|
63
49
|
- lib/saboteur/agent_skill.rb
|
|
50
|
+
- lib/saboteur/attribute_finder.rb
|
|
64
51
|
- lib/saboteur/baseline/acceptance.rb
|
|
65
52
|
- lib/saboteur/baseline/command.rb
|
|
66
53
|
- lib/saboteur/baseline/comparison.rb
|
|
@@ -126,6 +113,7 @@ files:
|
|
|
126
113
|
- lib/saboteur/engine/unscoped_suite_warning.rb
|
|
127
114
|
- lib/saboteur/engine/verdict_check.rb
|
|
128
115
|
- lib/saboteur/execution/external_runner.rb
|
|
116
|
+
- lib/saboteur/execution/fiddle_loader.rb
|
|
129
117
|
- lib/saboteur/execution/fork_readiness.rb
|
|
130
118
|
- lib/saboteur/execution/marker_sweep.rb
|
|
131
119
|
- lib/saboteur/execution/peak_memory.rb
|
|
@@ -138,6 +126,7 @@ files:
|
|
|
138
126
|
- lib/saboteur/execution/seatbelt.rb
|
|
139
127
|
- lib/saboteur/execution/stale_captures.rb
|
|
140
128
|
- lib/saboteur/execution/supervisor.rb
|
|
129
|
+
- lib/saboteur/execution/test_command_shape.rb
|
|
141
130
|
- lib/saboteur/execution/worker_pool.rb
|
|
142
131
|
- lib/saboteur/execution/wrapper.rb
|
|
143
132
|
- lib/saboteur/frameworks/database_hosts.rb
|
|
@@ -153,6 +142,7 @@ files:
|
|
|
153
142
|
- lib/saboteur/integration/focus_guard.rb
|
|
154
143
|
- lib/saboteur/integration/rspec.rb
|
|
155
144
|
- lib/saboteur/mutation_source/argument_removal.rb
|
|
145
|
+
- lib/saboteur/mutation_source/argument_swap.rb
|
|
156
146
|
- lib/saboteur/mutation_source/base.rb
|
|
157
147
|
- lib/saboteur/mutation_source/builtin.rb
|
|
158
148
|
- lib/saboteur/mutation_source/chain_removal.rb
|