saboteur 0.11.0 → 0.11.2
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 +86 -0
- data/README.md +2 -2
- data/lib/saboteur/baseline/command.rb +46 -2
- data/lib/saboteur/baseline/comparison.rb +6 -1
- data/lib/saboteur/baseline/completeness.rb +21 -0
- data/lib/saboteur/baseline/evidence.rb +11 -3
- data/lib/saboteur/baseline/loader.rb +18 -5
- data/lib/saboteur/baseline/manifest.rb +13 -4
- data/lib/saboteur/baseline/run_evidence.rb +3 -1
- data/lib/saboteur/baseline/workflow.rb +57 -9
- data/lib/saboteur/baseline_health.rb +4 -1
- data/lib/saboteur/cli/dirty_tree_notice.rb +7 -1
- data/lib/saboteur/cli/doctor_command.rb +10 -1
- data/lib/saboteur/cli/exit_status.rb +10 -5
- data/lib/saboteur/cli/explain_command.rb +51 -0
- data/lib/saboteur/cli/explicit_sources.rb +1 -1
- data/lib/saboteur/cli/failure_output.rb +2 -2
- data/lib/saboteur/cli/flaky_exit.rb +23 -0
- data/lib/saboteur/cli/presets.rb +11 -1
- data/lib/saboteur/cli/run_command.rb +17 -1
- data/lib/saboteur/cli/run_output.rb +10 -0
- data/lib/saboteur/cli/run_reporting.rb +13 -2
- data/lib/saboteur/cli/survivors_command.rb +82 -18
- data/lib/saboteur/cli.rb +3 -0
- data/lib/saboteur/config.rb +7 -2
- data/lib/saboteur/crash_report.rb +3 -2
- data/lib/saboteur/doctor.rb +11 -2
- data/lib/saboteur/engine/boot.rb +4 -2
- data/lib/saboteur/engine/coverage_store.rb +54 -3
- data/lib/saboteur/engine/distribution.rb +6 -2
- data/lib/saboteur/engine/pipelines.rb +14 -2
- data/lib/saboteur/engine/result_cache.rb +4 -2
- data/lib/saboteur/engine/scheduler.rb +23 -21
- data/lib/saboteur/engine/selection_announcement.rb +5 -4
- data/lib/saboteur/engine/subject_selection.rb +11 -1
- data/lib/saboteur/engine/survivor_confirmation.rb +1 -1
- data/lib/saboteur/engine/test_guess.rb +1 -1
- data/lib/saboteur/engine/thread_warnings.rb +9 -4
- data/lib/saboteur/engine.rb +57 -9
- data/lib/saboteur/execution/fiddle_loader.rb +29 -2
- data/lib/saboteur/help.rb +13 -1
- data/lib/saboteur/ignore_list.rb +36 -3
- data/lib/saboteur/install.rb +18 -3
- data/lib/saboteur/mutation_source/builtin.rb +1 -0
- data/lib/saboteur/mutation_source/operator_explain.rb +181 -0
- data/lib/saboteur/preview.rb +6 -1
- data/lib/saboteur/reporter/agent_commands.rb +29 -7
- data/lib/saboteur/reporter/agent_incomplete.rb +9 -2
- data/lib/saboteur/reporter/agent_summary.rb +2 -1
- data/lib/saboteur/reporter/baseline_tail.rb +9 -7
- data/lib/saboteur/reporter/gap_kinds.rb +80 -0
- data/lib/saboteur/reporter/html.rb +160 -10
- data/lib/saboteur/reporter/next_steps.rb +9 -7
- data/lib/saboteur/reporter/notice.rb +24 -0
- data/lib/saboteur/reporter/reuse_note.rb +37 -0
- data/lib/saboteur/reporter/review_comments.rb +17 -12
- data/lib/saboteur/reporter/score_table.rb +8 -7
- data/lib/saboteur/reporter/stryker.rb +11 -1
- data/lib/saboteur/reporter/survivor_classification.rb +26 -8
- data/lib/saboteur/reporter/survivor_hints.rb +3 -2
- data/lib/saboteur/reporter/survivor_list.rb +58 -4
- data/lib/saboteur/reporter/survivor_ranking.rb +13 -5
- data/lib/saboteur/reporter/terminal.rb +32 -33
- data/lib/saboteur/reporter/wording.rb +37 -0
- data/lib/saboteur/result.rb +39 -8
- data/lib/saboteur/selection/git_diff.rb +19 -1
- data/lib/saboteur/survivor_guidance.rb +33 -0
- data/lib/saboteur/text.rb +6 -0
- data/lib/saboteur/version.rb +1 -1
- metadata +10 -2
data/lib/saboteur/result.rb
CHANGED
|
@@ -72,6 +72,16 @@ module Saboteur
|
|
|
72
72
|
executed: executed, confirmation: confirmation)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# The reason a --flaky-check disagreement starts with: unverified, but
|
|
76
|
+
# not because a budget ran out (#1167).
|
|
77
|
+
FLAKY_PREFIX = "flaky verdict"
|
|
78
|
+
|
|
79
|
+
def self.flaky_reason?(reason) = reason.to_s.start_with?(FLAKY_PREFIX)
|
|
80
|
+
|
|
81
|
+
# Unverified because the reordered confirmation run disagreed, not
|
|
82
|
+
# because --max-duration cut the check short.
|
|
83
|
+
def flaky? = status == :unverified && self.class.flaky_reason?(reason)
|
|
84
|
+
|
|
75
85
|
# The same mutant, marked unverified: the budget ran out before its
|
|
76
86
|
# verdict could be checked, so it must not count as detected (#296).
|
|
77
87
|
def as_unverified(reason)
|
|
@@ -159,9 +169,9 @@ module Saboteur
|
|
|
159
169
|
MAX_ERROR_FRACTION = 0.5
|
|
160
170
|
|
|
161
171
|
attr_reader :results, :subjects, :wall_time, :invalid_mutations, :warnings, :live_announced, :root, :seed,
|
|
162
|
-
:skipped_subjects,
|
|
172
|
+
:skipped_subjects, :since,
|
|
163
173
|
:generation_events, :omissions, :run_evidence, :hazard_suppressions, :early_stop, :inline_suppressions,
|
|
164
|
-
:operator_policy, :discovery_failures, :memory, :shard, :test_command, :referenced
|
|
174
|
+
:operator_policy, :discovery_failures, :memory, :shard, :test_command, :referenced, :reuse_label
|
|
165
175
|
|
|
166
176
|
# REVIEW: diff mode; the terminal then shows one survivor per line.
|
|
167
177
|
# seed: the random seed every test run started from (nil when unknown).
|
|
@@ -170,7 +180,7 @@ module Saboteur
|
|
|
170
180
|
seed: nil, skipped_subjects: {}, diff_scope: nil, generation_events: [], omissions: [],
|
|
171
181
|
run_evidence: nil, hazard_suppressions: 0, early_stop: nil, matrix: false, inline_suppressions: [],
|
|
172
182
|
operator_policy: nil, discovery_failures: [], memory: nil, shard: nil,
|
|
173
|
-
test_command: nil, referenced: {})
|
|
183
|
+
test_command: nil, referenced: {}, reuse_label: nil, since: nil)
|
|
174
184
|
@results = results
|
|
175
185
|
@subjects = subjects
|
|
176
186
|
@wall_time = wall_time
|
|
@@ -182,15 +192,12 @@ module Saboteur
|
|
|
182
192
|
@seed = seed
|
|
183
193
|
@skipped_subjects = skipped_subjects
|
|
184
194
|
@diff_scope = diff_scope
|
|
185
|
-
|
|
186
|
-
@omissions = omissions.dup.freeze
|
|
195
|
+
take_frozen_copies(generation_events, omissions, inline_suppressions, discovery_failures)
|
|
187
196
|
@run_evidence = run_evidence
|
|
188
197
|
@hazard_suppressions = hazard_suppressions
|
|
189
198
|
@early_stop = early_stop
|
|
190
199
|
@matrix = matrix
|
|
191
|
-
@inline_suppressions = inline_suppressions.dup.freeze
|
|
192
200
|
@operator_policy = operator_policy
|
|
193
|
-
@discovery_failures = discovery_failures.dup.freeze
|
|
194
201
|
# Per-worker and supervisor peak RSS in MB, when the platform measured
|
|
195
202
|
# it (#202); nil elsewhere, so JSON reports omit the section.
|
|
196
203
|
@memory = memory.dup.freeze
|
|
@@ -198,9 +205,14 @@ module Saboteur
|
|
|
198
205
|
# The external command of a --test-command run, nil for in-process
|
|
199
206
|
# runs. Its presence means survivors carry no coverage map (#641).
|
|
200
207
|
@test_command = test_command
|
|
208
|
+
# The --since ref of a diff run, nil for a whole-project run. The
|
|
209
|
+
# expand hint and the HTML report echo it, so the scope survives the
|
|
210
|
+
# run that produced them.
|
|
211
|
+
@since = since
|
|
201
212
|
# file => specs, for files whose specs were chosen by constant reference
|
|
202
213
|
# or require instead of by name (zero-setup selection).
|
|
203
214
|
@referenced = referenced
|
|
215
|
+
@reuse_label = reuse_label
|
|
204
216
|
end
|
|
205
217
|
|
|
206
218
|
# A --matrix run: every covering test ran per mutant.
|
|
@@ -208,8 +220,16 @@ module Saboteur
|
|
|
208
220
|
@matrix
|
|
209
221
|
end
|
|
210
222
|
|
|
223
|
+
# The skipped subjects the live selection notice did not already name.
|
|
224
|
+
def unannounced_skips
|
|
225
|
+
skipped_subjects.reject do |subject, reason|
|
|
226
|
+
prefix = Text.display_safe_lines("#{subject.id} (#{subject.location(root)}): skipped (#{reason})")
|
|
227
|
+
live_announced.any? { |line| line.start_with?(prefix) }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
211
231
|
# True when the run was scoped to a diff (--since): the expand command
|
|
212
|
-
# echoes that scope.
|
|
232
|
+
# echoes that scope's ref.
|
|
213
233
|
def diff?
|
|
214
234
|
!@diff_scope.nil?
|
|
215
235
|
end
|
|
@@ -288,5 +308,16 @@ module Saboteur
|
|
|
288
308
|
timed = executed.reject(&:reused?).filter_map(&:runtime)
|
|
289
309
|
timed.empty? ? nil : timed.sum / timed.size
|
|
290
310
|
end
|
|
311
|
+
|
|
312
|
+
# The run lists arrive from builders that may reuse them; the report
|
|
313
|
+
# keeps its own frozen copies.
|
|
314
|
+
private
|
|
315
|
+
|
|
316
|
+
def take_frozen_copies(generation_events, omissions, inline_suppressions, discovery_failures)
|
|
317
|
+
@generation_events = generation_events.dup.freeze
|
|
318
|
+
@omissions = omissions.dup.freeze
|
|
319
|
+
@inline_suppressions = inline_suppressions.dup.freeze
|
|
320
|
+
@discovery_failures = discovery_failures.dup.freeze
|
|
321
|
+
end
|
|
291
322
|
end
|
|
292
323
|
end
|
|
@@ -15,6 +15,9 @@ module Saboteur
|
|
|
15
15
|
NO_FILE = "/dev/null" # rubocop:disable Style/FileNull -- literal in git diff output
|
|
16
16
|
HUNK = /\A@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
|
|
17
17
|
|
|
18
|
+
LOCAL_BRANCH_CAP = 5
|
|
19
|
+
BASE_NAMES = %w[main master trunk develop].freeze
|
|
20
|
+
|
|
18
21
|
module_function
|
|
19
22
|
|
|
20
23
|
# Fails before any boot work when REF is not a commit here, as a usage
|
|
@@ -38,12 +41,22 @@ module Saboteur
|
|
|
38
41
|
message = "--since #{ref}: not a commit in this repository " \
|
|
39
42
|
"(check the name; a shallow checkout may need a deeper fetch)"
|
|
40
43
|
branch = default_branch(root)
|
|
41
|
-
return message unless branch
|
|
44
|
+
return "#{message}#{local_branches_hint(root)}" unless branch
|
|
42
45
|
|
|
43
46
|
"#{message}; this repository's default branch is '#{branch}' - " \
|
|
44
47
|
"pass it explicitly: saboteur pr #{branch}"
|
|
45
48
|
end
|
|
46
49
|
|
|
50
|
+
# A local-only repo has no origin/HEAD to name: list the branches it
|
|
51
|
+
# does have, so a bare `saboteur pr` (default main) points at a real one.
|
|
52
|
+
def local_branches_hint(root)
|
|
53
|
+
out, _, ok = Open3.capture3("git", "for-each-ref", "--format=%(refname:short)", "refs/heads", chdir: root)
|
|
54
|
+
branches = ok.success? ? likely_bases(out.split("\n")).first(LOCAL_BRANCH_CAP) : []
|
|
55
|
+
return "" if branches.empty?
|
|
56
|
+
|
|
57
|
+
"; local branches: #{branches.join(', ')} - pass one explicitly: saboteur pr #{branches.first}"
|
|
58
|
+
end
|
|
59
|
+
|
|
47
60
|
# The default branch origin publishes, without the remote name, or nil
|
|
48
61
|
# when origin has no HEAD (a local-only clone keeps the plain hint).
|
|
49
62
|
def default_branch(root)
|
|
@@ -54,6 +67,11 @@ module Saboteur
|
|
|
54
67
|
branch.empty? ? nil : branch
|
|
55
68
|
end
|
|
56
69
|
|
|
70
|
+
# Conventional base names first, so the suggested example is a base.
|
|
71
|
+
def likely_bases(branches)
|
|
72
|
+
branches.sort_by { |name| [BASE_NAMES.index(name) || BASE_NAMES.size, name] }
|
|
73
|
+
end
|
|
74
|
+
|
|
57
75
|
def merge_base(ref, root:)
|
|
58
76
|
git(root, "merge-base", ref, "HEAD").strip
|
|
59
77
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Saboteur
|
|
5
|
+
# One wording for the two survivor follow-ups a user copies: replaying
|
|
6
|
+
# one mutant and accepting one in the ignore file. The run screen (Next
|
|
7
|
+
# steps) and `saboteur survivors --show` share these strings so the two
|
|
8
|
+
# surfaces never drift (#1167).
|
|
9
|
+
module SurvivorGuidance
|
|
10
|
+
REPLAY_CAVEAT = "(add this run's own --config, --since, --subject, --test and source options)"
|
|
11
|
+
IGNORE_INTRO = "accept an equivalent one: add to your ignore file " \
|
|
12
|
+
"(c.ignore_file, default #{Config::IGNORE_FILE}):".freeze
|
|
13
|
+
REASON_PLACEHOLDER = "why it is equivalent"
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def replay_command(digest, seed: nil)
|
|
18
|
+
"saboteur run --id #{digest}#{" --seed #{seed}" if seed}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# One line, paste-ready. todo names what the user must still fill in
|
|
22
|
+
# (a capped report omits the subject for some digests).
|
|
23
|
+
def ignore_entry(digest, subject_id, todo: nil)
|
|
24
|
+
entry = %(i.mutation "#{digest}", subject: "#{subject_id}", reason: "#{REASON_PLACEHOLDER}")
|
|
25
|
+
todo ? "#{entry} # TODO: #{todo}" : entry
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The entry wrapped so it pastes into the ignore file as-is (#1194).
|
|
29
|
+
def ignore_block(digest, subject_id, todo: nil)
|
|
30
|
+
"Saboteur.ignore do |i|\n #{ignore_entry(digest, subject_id, todo: todo)}\nend"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/saboteur/text.rb
CHANGED
|
@@ -11,6 +11,12 @@ module Saboteur
|
|
|
11
11
|
module Text
|
|
12
12
|
module_function
|
|
13
13
|
|
|
14
|
+
# "1 subject" / "2 subjects": the count with the noun in the right
|
|
15
|
+
# number (review C9). An irregular plural passes explicitly.
|
|
16
|
+
def counted(count, singular, plural = "#{singular}s")
|
|
17
|
+
"#{count} #{count == 1 ? singular : plural}"
|
|
18
|
+
end
|
|
19
|
+
|
|
14
20
|
# A list for a warning or note: the first five items, then "and N more".
|
|
15
21
|
# One rule for every message that names files, subjects or specs (#332).
|
|
16
22
|
# Items are repo-controlled text, so each is display_safe (#834).
|
data/lib/saboteur/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: saboteur
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.11.
|
|
4
|
+
version: 0.11.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vamsi Pavan Mahesh Gunturu
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|
|
@@ -68,9 +68,11 @@ files:
|
|
|
68
68
|
- lib/saboteur/cli/error_cause.rb
|
|
69
69
|
- lib/saboteur/cli/execution_options.rb
|
|
70
70
|
- lib/saboteur/cli/exit_status.rb
|
|
71
|
+
- lib/saboteur/cli/explain_command.rb
|
|
71
72
|
- lib/saboteur/cli/explicit_file_options.rb
|
|
72
73
|
- lib/saboteur/cli/explicit_sources.rb
|
|
73
74
|
- lib/saboteur/cli/failure_output.rb
|
|
75
|
+
- lib/saboteur/cli/flaky_exit.rb
|
|
74
76
|
- lib/saboteur/cli/help_command.rb
|
|
75
77
|
- lib/saboteur/cli/help_options.rb
|
|
76
78
|
- lib/saboteur/cli/history_command.rb
|
|
@@ -203,6 +205,7 @@ files:
|
|
|
203
205
|
- lib/saboteur/mutation_source/operand_replacement.rb
|
|
204
206
|
- lib/saboteur/mutation_source/operator_assignment.rb
|
|
205
207
|
- lib/saboteur/mutation_source/operator_examples.rb
|
|
208
|
+
- lib/saboteur/mutation_source/operator_explain.rb
|
|
206
209
|
- lib/saboteur/mutation_source/operator_registry.rb
|
|
207
210
|
- lib/saboteur/mutation_source/operator_visit.rb
|
|
208
211
|
- lib/saboteur/mutation_source/pair_removal.rb
|
|
@@ -224,6 +227,7 @@ files:
|
|
|
224
227
|
- lib/saboteur/reporter/baseline_tail.rb
|
|
225
228
|
- lib/saboteur/reporter/diff.rb
|
|
226
229
|
- lib/saboteur/reporter/findings.rb
|
|
230
|
+
- lib/saboteur/reporter/gap_kinds.rb
|
|
227
231
|
- lib/saboteur/reporter/history_paths.rb
|
|
228
232
|
- lib/saboteur/reporter/history_policy.rb
|
|
229
233
|
- lib/saboteur/reporter/history_reader.rb
|
|
@@ -232,8 +236,10 @@ files:
|
|
|
232
236
|
- lib/saboteur/reporter/kill_causes.rb
|
|
233
237
|
- lib/saboteur/reporter/mutation_text.rb
|
|
234
238
|
- lib/saboteur/reporter/next_steps.rb
|
|
239
|
+
- lib/saboteur/reporter/notice.rb
|
|
235
240
|
- lib/saboteur/reporter/palette.rb
|
|
236
241
|
- lib/saboteur/reporter/replay.rb
|
|
242
|
+
- lib/saboteur/reporter/reuse_note.rb
|
|
237
243
|
- lib/saboteur/reporter/review_comments.rb
|
|
238
244
|
- lib/saboteur/reporter/safe_out.rb
|
|
239
245
|
- lib/saboteur/reporter/score_history.rb
|
|
@@ -245,6 +251,7 @@ files:
|
|
|
245
251
|
- lib/saboteur/reporter/survivor_ranking.rb
|
|
246
252
|
- lib/saboteur/reporter/terminal.rb
|
|
247
253
|
- lib/saboteur/reporter/test_matrix.rb
|
|
254
|
+
- lib/saboteur/reporter/wording.rb
|
|
248
255
|
- lib/saboteur/result.rb
|
|
249
256
|
- lib/saboteur/score.rb
|
|
250
257
|
- lib/saboteur/selection/coverage_map.rb
|
|
@@ -258,6 +265,7 @@ files:
|
|
|
258
265
|
- lib/saboteur/selection/untracked_changes.rb
|
|
259
266
|
- lib/saboteur/subject.rb
|
|
260
267
|
- lib/saboteur/subject_finder.rb
|
|
268
|
+
- lib/saboteur/survivor_guidance.rb
|
|
261
269
|
- lib/saboteur/text.rb
|
|
262
270
|
- lib/saboteur/version.rb
|
|
263
271
|
- lib/saboteur/working_tree.rb
|