saboteur 0.5.0 → 0.7.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +399 -0
  3. data/README.md +202 -42
  4. data/exe/saboteur +34 -7
  5. data/lib/saboteur/baseline/comparison.rb +1 -0
  6. data/lib/saboteur/baseline/evidence.rb +11 -1
  7. data/lib/saboteur/baseline/run_evidence.rb +1 -1
  8. data/lib/saboteur/baseline/workflow.rb +2 -1
  9. data/lib/saboteur/baseline_health.rb +40 -6
  10. data/lib/saboteur/cli/exit_status.rb +49 -4
  11. data/lib/saboteur/cli/help_options.rb +16 -0
  12. data/lib/saboteur/cli/presets.rb +36 -0
  13. data/lib/saboteur/cli/run_budget_options.rb +37 -0
  14. data/lib/saboteur/cli/run_reporting.rb +75 -6
  15. data/lib/saboteur/cli.rb +80 -48
  16. data/lib/saboteur/config.rb +88 -13
  17. data/lib/saboteur/crash_report.rb +154 -0
  18. data/lib/saboteur/custom_map.rb +3 -1
  19. data/lib/saboteur/doctor.rb +64 -2
  20. data/lib/saboteur/engine/baseline_guard.rb +79 -5
  21. data/lib/saboteur/engine/boot.rb +12 -1
  22. data/lib/saboteur/engine/canary.rb +41 -10
  23. data/lib/saboteur/engine/candidate_selection.rb +9 -1
  24. data/lib/saboteur/engine/candidates.rb +4 -1
  25. data/lib/saboteur/engine/coverage_guard.rb +29 -0
  26. data/lib/saboteur/engine/coverage_store.rb +48 -6
  27. data/lib/saboteur/engine/diagnostics.rb +23 -2
  28. data/lib/saboteur/engine/diff_scope.rb +24 -5
  29. data/lib/saboteur/engine/distribution.rb +124 -8
  30. data/lib/saboteur/engine/early_stop_recording.rb +46 -0
  31. data/lib/saboteur/engine/result_cache.rb +2 -1
  32. data/lib/saboteur/engine/run_budget.rb +63 -0
  33. data/lib/saboteur/engine/run_fingerprint.rb +5 -1
  34. data/lib/saboteur/engine/sandbox_policy.rb +18 -11
  35. data/lib/saboteur/engine/scheduler.rb +114 -18
  36. data/lib/saboteur/engine/subject_selection.rb +84 -5
  37. data/lib/saboteur/engine/suite_files.rb +3 -1
  38. data/lib/saboteur/engine/test_guess.rb +183 -23
  39. data/lib/saboteur/engine/thread_warnings.rb +53 -0
  40. data/lib/saboteur/engine/unscoped_suite_warning.rb +41 -0
  41. data/lib/saboteur/engine/verdict_check.rb +54 -18
  42. data/lib/saboteur/engine.rb +175 -96
  43. data/lib/saboteur/execution/protocol.rb +2 -1
  44. data/lib/saboteur/execution/runner.rb +179 -17
  45. data/lib/saboteur/execution/sandbox.rb +6 -1
  46. data/lib/saboteur/execution/supervisor.rb +18 -3
  47. data/lib/saboteur/execution/worker_pool.rb +87 -7
  48. data/lib/saboteur/frameworks/database_hosts.rb +30 -0
  49. data/lib/saboteur/frameworks/rails.rb +8 -10
  50. data/lib/saboteur/help.rb +55 -10
  51. data/lib/saboteur/ignore_list.rb +11 -1
  52. data/lib/saboteur/install.rb +4 -3
  53. data/lib/saboteur/integration/base.rb +7 -2
  54. data/lib/saboteur/integration/rspec.rb +40 -4
  55. data/lib/saboteur/mutation_source/builtin.rb +22 -13
  56. data/lib/saboteur/mutation_source/hazard_guard.rb +21 -3
  57. data/lib/saboteur/mutation_source/interpolation_removal.rb +41 -0
  58. data/lib/saboteur/mutation_source/method_swap.rb +45 -0
  59. data/lib/saboteur/mutation_source/mutation.rb +5 -3
  60. data/lib/saboteur/mutation_source/operand_replacement.rb +54 -0
  61. data/lib/saboteur/mutation_source/operator_examples.rb +38 -0
  62. data/lib/saboteur/mutation_source/operator_registry.rb +36 -8
  63. data/lib/saboteur/mutation_source/operator_visit.rb +39 -0
  64. data/lib/saboteur/preview.rb +12 -2
  65. data/lib/saboteur/reporter/agent_commands.rb +49 -16
  66. data/lib/saboteur/reporter/agent_incomplete.rb +27 -2
  67. data/lib/saboteur/reporter/agent_summary.rb +1 -1
  68. data/lib/saboteur/reporter/history_policy.rb +35 -0
  69. data/lib/saboteur/reporter/history_record.rb +4 -5
  70. data/lib/saboteur/reporter/next_steps.rb +14 -4
  71. data/lib/saboteur/reporter/score_history.rb +19 -3
  72. data/lib/saboteur/reporter/stryker.rb +19 -8
  73. data/lib/saboteur/reporter/survivor_hints.rb +3 -0
  74. data/lib/saboteur/reporter/terminal.rb +43 -10
  75. data/lib/saboteur/result.rb +25 -7
  76. data/lib/saboteur/selection/coverage_map.rb +2 -1
  77. data/lib/saboteur/selection/git_diff.rb +15 -5
  78. data/lib/saboteur/selection/untracked_changes.rb +126 -0
  79. data/lib/saboteur/subject.rb +11 -0
  80. data/lib/saboteur/text.rb +42 -0
  81. data/lib/saboteur/version.rb +1 -1
  82. data/lib/saboteur.rb +16 -1
  83. metadata +20 -2
@@ -12,7 +12,10 @@ module Saboteur
12
12
  # test can observe a change there, so it is outside the score.
13
13
  # unreachable: a Method object captured before re-evaluation still calls
14
14
  # the original body, so no test can observe the mutant. Outside the score.
15
- STATUSES = %i[killed survived timeout resource_limit error skipped ran_at_load unreachable].freeze
15
+ # unverified: the --max-duration budget ran out before the verdict check
16
+ # or timeout calibration could run, so the kill or timeout proves
17
+ # nothing. Outside the score; the run reports budget-partial.
18
+ STATUSES = %i[killed survived timeout resource_limit error skipped ran_at_load unreachable unverified].freeze
16
19
 
17
20
  # Outcome of one mutation. reason carries the skip reason, the error
18
21
  # message or the timeout limit. ignored is the ignore-list reason, if any.
@@ -40,6 +43,14 @@ module Saboteur
40
43
  @ignored = nil
41
44
  end
42
45
 
46
+ # The same mutant, marked unverified: the budget ran out before its
47
+ # verdict could be checked, so it must not count as detected (#296).
48
+ def as_unverified(reason)
49
+ MutantResult.new(subject: subject, mutation: mutation, status: :unverified, mutated_source: mutated_source,
50
+ reason: reason, runtime: runtime, killed_by: killed_by, covered_by: covered_by,
51
+ kill_cause: kill_cause, executed: executed)
52
+ end
53
+
43
54
  # The same mutant, marked as an error: its verdict did not hold up.
44
55
  def as_error(reason, rejected_kill: false)
45
56
  MutantResult.new(subject: subject, mutation: mutation, status: :error, mutated_source: mutated_source,
@@ -100,10 +111,13 @@ module Saboteur
100
111
  end
101
112
  end
102
113
 
103
- # A run that stopped before every selected subject was mutated (--fail-fast).
104
- # unreached lists the subject ids that never ran; the verdict covers only
105
- # the subjects that did.
106
- EarlyStop = Data.define(:reason, :unreached)
114
+ # A run that stopped before every selected subject was mutated (--fail-fast,
115
+ # --max-duration), or whose budget ran out before every verdict check.
116
+ # unreached lists the subject ids that never ran; truncated lists the
117
+ # subjects that started but stopped mid-way as {subject:, omitted:}
118
+ # entries; unverified lists the subjects whose kills the budget left
119
+ # unchecked. The verdict covers only completed, verified work.
120
+ EarlyStop = Data.define(:reason, :unreached, :truncated, :unverified)
107
121
 
108
122
  # Everything one run produced.
109
123
  class Report
@@ -112,13 +126,15 @@ module Saboteur
112
126
  MAX_ERROR_FRACTION = 0.5
113
127
 
114
128
  attr_reader :results, :subjects, :wall_time, :invalid_mutations, :warnings, :root, :seed, :skipped_subjects,
115
- :generation_events, :omissions, :run_evidence, :hazard_suppressions, :early_stop, :inline_suppressions
129
+ :generation_events, :omissions, :run_evidence, :hazard_suppressions, :early_stop, :inline_suppressions,
130
+ :operator_policy, :discovery_failures
116
131
 
117
132
  # REVIEW: diff mode; the terminal then shows one survivor per line.
118
133
  # seed: the random seed every test run started from (nil when unknown).
119
134
  def initialize(results:, subjects:, wall_time:, root:, invalid_mutations: 0, warnings: [], review: false,
120
135
  seed: nil, skipped_subjects: {}, diff_scope: nil, generation_events: [], omissions: [],
121
- run_evidence: nil, hazard_suppressions: 0, early_stop: nil, matrix: false, inline_suppressions: [])
136
+ run_evidence: nil, hazard_suppressions: 0, early_stop: nil, matrix: false, inline_suppressions: [],
137
+ operator_policy: nil, discovery_failures: [])
122
138
  @results = results
123
139
  @subjects = subjects
124
140
  @wall_time = wall_time
@@ -136,6 +152,8 @@ module Saboteur
136
152
  @early_stop = early_stop
137
153
  @matrix = matrix
138
154
  @inline_suppressions = inline_suppressions.dup.freeze
155
+ @operator_policy = operator_policy
156
+ @discovery_failures = discovery_failures.dup.freeze
139
157
  end
140
158
 
141
159
  # A --matrix run: every covering test ran per mutant.
@@ -5,6 +5,7 @@ require "coverage"
5
5
  require "digest"
6
6
  require "fileutils"
7
7
  require "json"
8
+ require_relative "../text"
8
9
 
9
10
  module Saboteur
10
11
  module Selection
@@ -168,7 +169,7 @@ module Saboteur
168
169
  end
169
170
 
170
171
  def self.load(path)
171
- from_json(File.read(path)) if File.file?(path)
172
+ from_json(Text.utf8_strict(File.read(path))) if File.file?(path)
172
173
  rescue JSON::ParserError, Error
173
174
  nil
174
175
  end
@@ -2,6 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "open3"
5
+ require_relative "../text"
6
+ require_relative "untracked_changes"
5
7
 
6
8
  module Saboteur
7
9
  module Selection
@@ -39,12 +41,18 @@ module Saboteur
39
41
  status.success? ? out.b : nil
40
42
  end
41
43
 
42
- def changed_lines(ref, root:)
44
+ def changed_lines(ref, root:, include: nil, diagnostics: nil)
43
45
  verify_ref!(ref, root: root)
44
46
  base = merge_base(ref, root: root)
45
- top = git(root, "rev-parse", "--show-toplevel").strip
46
- parse(git(root, "-c", "core.quotePath=false", "diff", "--unified=0", "--no-color", "--no-ext-diff", base,
47
- "--"), top)
47
+ # Root and paths are filesystem identity: strip as bytes and retag
48
+ # losslessly (strip raises on locale-tagged high bytes; a scrub
49
+ # would rename a raw-byte path). Parse the diff body as bytes and
50
+ # tag each extracted path UTF-8 at the boundary.
51
+ top = Text.utf8_identity(git(root, "rev-parse", "--show-toplevel").b.strip)
52
+ changes = parse(git(root, "-c", "core.quotePath=false", "diff", "--unified=0", "--no-color",
53
+ "--no-ext-diff", base, "--").b, top)
54
+ UntrackedChanges.union(changes, top, include: include, diagnostics: diagnostics)
55
+ changes
48
56
  end
49
57
 
50
58
  def parse(diff, top)
@@ -52,8 +60,10 @@ module Saboteur
52
60
  file = nil
53
61
  diff.each_line do |line|
54
62
  if line.start_with?("+++ ")
55
- path = unquote(line.chomp.delete_prefix("+++ ").delete_suffix("\t"))
63
+ path = Text.utf8_identity(unquote(line.chomp.delete_prefix("+++ ").delete_suffix("\t")))
56
64
  file = path == NO_FILE ? nil : File.join(top, path.delete_prefix("b/"))
65
+ # HUNK is anchored, so a non-hunk line - including a raw-byte
66
+ # content line - fails the match at its first byte.
57
67
  elsif file && (match = HUNK.match(line))
58
68
  start = match[1].to_i
59
69
  count = (match[2] || "1").to_i
@@ -0,0 +1,126 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../text"
5
+
6
+ module Saboteur
7
+ module Selection
8
+ # Untracked files as changed-line coverage for --since.
9
+ #
10
+ # A diff cannot see untracked files, so a never-committed source file
11
+ # would silently contribute no changed lines - and, being in no map,
12
+ # not even the changed-files-without-subjects warning (#295). Union
13
+ # every untracked file that could hold subjects (the include set the
14
+ # run will mutate) as fully changed: a new file is the strongest
15
+ # possible "changed since ref" signal. Ignored files stay invisible
16
+ # (--exclude-standard), and binary or empty files carry no lines,
17
+ # matching how a committed binary diffs to no hunks.
18
+ #
19
+ # Each file is opened RDONLY|NOFOLLOW|NONBLOCK and validated on that
20
+ # one descriptor, closing the listing-to-read race: a symlink swapped
21
+ # in after the listing fails the open, a FIFO opens without blocking
22
+ # and fails the regular-file stat, and a vanished or unreadable file
23
+ # is reported. Where a platform lacks either flag the union is skipped
24
+ # with a warning rather than done unsafely (the crash log makes the
25
+ # same trade, #296). The binary sniff reads only the first few
26
+ # kilobytes (git's own heuristic window) and the count streams, so a
27
+ # huge untracked source costs one pass, not its content in memory; the
28
+ # map holds a range, not one Set entry per line.
29
+ module UntrackedChanges
30
+ BINARY_SNIFF_BYTES = 8000
31
+ READ_CHUNK_BYTES = 64 * 1024
32
+
33
+ module_function
34
+
35
+ # Add every include-eligible untracked file to CHANGES as a full
36
+ # range. Eligible files that cannot be read are warned about and
37
+ # recorded as discovery failures, not dropped silently, so the run is
38
+ # reported incomplete instead of clean over mixed source.
39
+ def union(changes, top, include:, diagnostics:)
40
+ unless File.const_defined?(:NOFOLLOW) && File.const_defined?(:NONBLOCK)
41
+ diagnostics&.warn("--since: untracked files are not included: this platform lacks " \
42
+ "O_NOFOLLOW/O_NONBLOCK, so they cannot be opened race-free")
43
+ return
44
+ end
45
+
46
+ flags = File::RDONLY | File::NOFOLLOW | File::NONBLOCK
47
+ unreadable = []
48
+ files(top).each do |name|
49
+ file = File.join(top, name)
50
+ # Membership is lexical first: an included path swapped to a
51
+ # symlink must reach the safe open, which rejects it, so the swap
52
+ # is reported instead of filtered out on the resolved target. The
53
+ # canonical fallback keeps a symlinked ROOT in scope, because the
54
+ # include set is realpath'd.
55
+ next if include && !include.include?(file) && !include.include?(CoverageMap.canonical(file))
56
+
57
+ case (lines = line_count(file, flags))
58
+ when :unreadable then unreadable << name
59
+ when Integer then changes[file] = (1..lines)
60
+ end
61
+ end
62
+ unreadable.each do |name|
63
+ diagnostics&.record_discovery_failure(File.join(top, name), reason: "unreadable")
64
+ end
65
+ warn_unreadable(unreadable, diagnostics)
66
+ end
67
+
68
+ # Absolute paths of untracked, non-ignored files under ROOT: they
69
+ # never appear in the diff but can still sit in excluded territory.
70
+ def paths(root)
71
+ top = Text.utf8_identity(GitDiff.git(root, "rev-parse", "--show-toplevel").b.strip)
72
+ files(top).map { |name| File.join(top, name) }
73
+ end
74
+
75
+ def files(top)
76
+ GitDiff.git(top, "ls-files", "-z", "--others", "--exclude-standard").b.split("\0").map do |name|
77
+ Text.utf8_identity(name)
78
+ end
79
+ end
80
+
81
+ # An included untracked file that cannot be read (vanished,
82
+ # unreadable or replaced after the listing) is missing source
83
+ # coverage: a clean --since must not hide it.
84
+ def warn_unreadable(names, diagnostics)
85
+ return if names.empty? || diagnostics.nil?
86
+
87
+ shown = names.sort.first(5).join(", ")
88
+ shown += ", ..." if names.size > 5
89
+ diagnostics.warn("--since: #{names.size} included untracked file(s) could not be read (vanished, " \
90
+ "unreadable or replaced), so their subjects are not covered: #{shown}")
91
+ end
92
+
93
+ # Line count of an untracked file opened race-free: nil when the
94
+ # file stays out of the map by content (empty, or binary inside the
95
+ # sniff window), or :unreadable when it could not be validated
96
+ # (vanished, unreadable, a symlink, not a regular file).
97
+ def line_count(file, flags)
98
+ File.open(file, flags) do |io|
99
+ next :unreadable unless io.stat.file?
100
+
101
+ count_lines(io)
102
+ end
103
+ rescue SystemCallError, IOError
104
+ :unreadable
105
+ end
106
+
107
+ # Every read is bounded: the binary sniff takes only the first few
108
+ # kilobytes (git's own heuristic window; read of an empty file is
109
+ # nil, never ""), then the count streams in fixed chunks.
110
+ def count_lines(io)
111
+ first = io.read(BINARY_SNIFF_BYTES)
112
+ return if first.nil? || first.include?("\0")
113
+
114
+ lines = first.count("\n")
115
+ trailing_newline = first.end_with?("\n")
116
+ # A no-arg read returns "" at EOF, never nil - guard the loop so a
117
+ # mutant that drops the chunk size spins zero times, not forever.
118
+ while (chunk = io.read(READ_CHUNK_BYTES)) && !chunk.empty?
119
+ lines += chunk.count("\n")
120
+ trailing_newline = chunk.end_with?("\n")
121
+ end
122
+ trailing_newline ? lines : lines + 1
123
+ end
124
+ end
125
+ end
126
+ end
@@ -114,6 +114,17 @@ module Saboteur
114
114
  line.between?(start_line, end_line)
115
115
  end
116
116
 
117
+ # True when a changed-lines set intersects this subject's span. A
118
+ # whole-file range answers by interval overlap in O(1) instead of a
119
+ # per-line scan from line 1 for every subject.
120
+ def overlaps_lines?(lines)
121
+ if lines.is_a?(Range)
122
+ lines.begin <= end_line && start_line <= lines.end
123
+ else
124
+ lines.any? { |line| contains_line?(line) }
125
+ end
126
+ end
127
+
117
128
  # Applies the mutation's edits to this method's text and returns the
118
129
  # mutated method text. Raises ArgumentError for edits outside the method
119
130
  # or overlapping each other.
@@ -0,0 +1,42 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ module Saboteur
7
+ # Bytes arriving from outside the process (git output, cache and log files,
8
+ # worker pipes) are tagged with the process locale. Under a non-UTF-8
9
+ # locale such as LANG=C that tag is US-ASCII, where any non-ASCII byte
10
+ # raises on use. Re-tag everything crossing the boundary as UTF-8.
11
+ module Text
12
+ module_function
13
+
14
+ # Lossy: bytes that are not valid UTF-8 become the replacement
15
+ # character. For log and message display only - never feed the result
16
+ # back into filesystem, cache or protocol identity: scrubbing rewrites
17
+ # raw bytes, which can alias two different names or make a path vanish
18
+ # from a File.exist? probe.
19
+ def utf8(string)
20
+ string.dup.force_encoding(Encoding::UTF_8).scrub
21
+ end
22
+
23
+ # Lossless: every byte survives; only the encoding tag changes. Use for
24
+ # paths and identity-bearing fields. File.* calls and String == then
25
+ # work on the exact bytes git reported; only rendering (regexes over
26
+ # arbitrary content, JSON, terminal output) needs #utf8.
27
+ def utf8_identity(string)
28
+ string.dup.force_encoding(Encoding::UTF_8)
29
+ end
30
+
31
+ # Lossless + strict: retag without scrubbing, then reject bytes
32
+ # that are not valid UTF-8 with JSON::ParserError, so a JSON
33
+ # boundary's existing parse-error handling rejects corrupt input
34
+ # instead of silently accepting a rewritten version of it.
35
+ def utf8_strict(string)
36
+ tagged = utf8_identity(string)
37
+ raise JSON::ParserError, "invalid UTF-8" unless tagged.valid_encoding?
38
+
39
+ tagged
40
+ end
41
+ end
42
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Saboteur
5
- VERSION = "0.5.0"
5
+ VERSION = "0.7.0"
6
6
  end
data/lib/saboteur.rb CHANGED
@@ -6,11 +6,26 @@ require_relative "saboteur/version"
6
6
  module Saboteur
7
7
  class Error < StandardError; end
8
8
 
9
+ # A phase timeout that --max-duration shortened: budget exhaustion, not a
10
+ # broken suite. The engine turns it into the budget partial-report path
11
+ # instead of a phase error (#296).
12
+ class BudgetPhaseTimeout < Error; end
13
+
9
14
  # Invalid configuration or input files (exit code 2).
10
15
  class ConfigError < Error; end
11
16
 
12
17
  # Files vanished from the working tree while mutants ran (exit code 3).
13
- class CanaryTripped < Error; end
18
+ class CanaryTripped < Error
19
+ # Set by the per-mutant probe (the mutant whose run just ended, #235);
20
+ # nil for a subject-end check, whose "most recent mutant" is only the
21
+ # likely cause and must not outrank a per-mutant report (#285).
22
+ attr_reader :culprit_digest
23
+
24
+ def initialize(message = nil, culprit_digest: nil)
25
+ @culprit_digest = culprit_digest
26
+ super(message)
27
+ end
28
+ end
14
29
 
15
30
  class << self
16
31
  # Set while hook files from config load, so they can call Saboteur.hook.
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.5.0
4
+ version: 0.7.0
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-14 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fiddle
@@ -73,9 +73,13 @@ files:
73
73
  - lib/saboteur/baseline_health.rb
74
74
  - lib/saboteur/cli.rb
75
75
  - lib/saboteur/cli/exit_status.rb
76
+ - lib/saboteur/cli/help_options.rb
77
+ - lib/saboteur/cli/presets.rb
78
+ - lib/saboteur/cli/run_budget_options.rb
76
79
  - lib/saboteur/cli/run_reporting.rb
77
80
  - lib/saboteur/config.rb
78
81
  - lib/saboteur/config_file.rb
82
+ - lib/saboteur/crash_report.rb
79
83
  - lib/saboteur/custom_map.rb
80
84
  - lib/saboteur/doctor.rb
81
85
  - lib/saboteur/engine.rb
@@ -85,19 +89,24 @@ files:
85
89
  - lib/saboteur/engine/canary.rb
86
90
  - lib/saboteur/engine/candidate_selection.rb
87
91
  - lib/saboteur/engine/candidates.rb
92
+ - lib/saboteur/engine/coverage_guard.rb
88
93
  - lib/saboteur/engine/coverage_store.rb
89
94
  - lib/saboteur/engine/data_inputs.rb
90
95
  - lib/saboteur/engine/diagnostics.rb
91
96
  - lib/saboteur/engine/diff_scope.rb
92
97
  - lib/saboteur/engine/distribution.rb
98
+ - lib/saboteur/engine/early_stop_recording.rb
93
99
  - lib/saboteur/engine/result_cache.rb
100
+ - lib/saboteur/engine/run_budget.rb
94
101
  - lib/saboteur/engine/run_fingerprint.rb
95
102
  - lib/saboteur/engine/sandbox_policy.rb
96
103
  - lib/saboteur/engine/scheduler.rb
97
104
  - lib/saboteur/engine/subject_selection.rb
98
105
  - lib/saboteur/engine/suite_files.rb
99
106
  - lib/saboteur/engine/test_guess.rb
107
+ - lib/saboteur/engine/thread_warnings.rb
100
108
  - lib/saboteur/engine/unchanged_syntax.rb
109
+ - lib/saboteur/engine/unscoped_suite_warning.rb
101
110
  - lib/saboteur/engine/verdict_check.rb
102
111
  - lib/saboteur/execution/fork_readiness.rb
103
112
  - lib/saboteur/execution/process_table.rb
@@ -110,6 +119,7 @@ files:
110
119
  - lib/saboteur/execution/supervisor.rb
111
120
  - lib/saboteur/execution/worker_pool.rb
112
121
  - lib/saboteur/execution/wrapper.rb
122
+ - lib/saboteur/frameworks/database_hosts.rb
113
123
  - lib/saboteur/frameworks/rails.rb
114
124
  - lib/saboteur/gates.rb
115
125
  - lib/saboteur/help.rb
@@ -129,10 +139,15 @@ files:
129
139
  - lib/saboteur/mutation_source/endpoint_replacement.rb
130
140
  - lib/saboteur/mutation_source/filter_replacement.rb
131
141
  - lib/saboteur/mutation_source/hazard_guard.rb
142
+ - lib/saboteur/mutation_source/interpolation_removal.rb
132
143
  - lib/saboteur/mutation_source/literal_replacement.rb
144
+ - lib/saboteur/mutation_source/method_swap.rb
133
145
  - lib/saboteur/mutation_source/mutation.rb
146
+ - lib/saboteur/mutation_source/operand_replacement.rb
134
147
  - lib/saboteur/mutation_source/operator_assignment.rb
148
+ - lib/saboteur/mutation_source/operator_examples.rb
135
149
  - lib/saboteur/mutation_source/operator_registry.rb
150
+ - lib/saboteur/mutation_source/operator_visit.rb
136
151
  - lib/saboteur/mutation_source/pair_removal.rb
137
152
  - lib/saboteur/mutation_source/range_replacement.rb
138
153
  - lib/saboteur/preview.rb
@@ -145,6 +160,7 @@ files:
145
160
  - lib/saboteur/reporter/diff.rb
146
161
  - lib/saboteur/reporter/findings.rb
147
162
  - lib/saboteur/reporter/history_paths.rb
163
+ - lib/saboteur/reporter/history_policy.rb
148
164
  - lib/saboteur/reporter/history_record.rb
149
165
  - lib/saboteur/reporter/html.rb
150
166
  - lib/saboteur/reporter/mutation_text.rb
@@ -164,8 +180,10 @@ files:
164
180
  - lib/saboteur/selection/coverage_map.rb
165
181
  - lib/saboteur/selection/git_diff.rb
166
182
  - lib/saboteur/selection/recorder.rb
183
+ - lib/saboteur/selection/untracked_changes.rb
167
184
  - lib/saboteur/subject.rb
168
185
  - lib/saboteur/subject_finder.rb
186
+ - lib/saboteur/text.rb
169
187
  - lib/saboteur/version.rb
170
188
  - lib/saboteur/working_tree.rb
171
189
  homepage: https://github.com/vamsipavanmahesh/saboteur