saboteur 0.2.0 → 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 +40 -0
- data/README.md +322 -54
- data/lib/saboteur/baseline_health.rb +116 -0
- data/lib/saboteur/cli.rb +169 -22
- data/lib/saboteur/config.rb +39 -5
- data/lib/saboteur/doctor.rb +72 -0
- data/lib/saboteur/engine/baseline_guard.rb +99 -0
- data/lib/saboteur/engine/candidate_selection.rb +35 -0
- data/lib/saboteur/engine/coverage_store.rb +9 -2
- data/lib/saboteur/engine/distribution.rb +4 -2
- data/lib/saboteur/engine/run_fingerprint.rb +316 -0
- data/lib/saboteur/engine/verdict_check.rb +4 -2
- data/lib/saboteur/engine.rb +53 -14
- data/lib/saboteur/execution/fork_readiness.rb +46 -0
- data/lib/saboteur/help.rb +194 -0
- data/lib/saboteur/integration/base.rb +8 -0
- data/lib/saboteur/integration/rspec.rb +23 -2
- data/lib/saboteur/mutation_source/builtin.rb +6 -1
- data/lib/saboteur/mutation_source/filter_replacement.rb +60 -0
- data/lib/saboteur/mutation_source/literal_replacement.rb +74 -0
- data/lib/saboteur/replay.rb +33 -0
- data/lib/saboteur/reporter/findings.rb +123 -0
- data/lib/saboteur/reporter/html.rb +165 -0
- data/lib/saboteur/reporter/replay.rb +23 -0
- data/lib/saboteur/reporter/stryker.rb +11 -1
- data/lib/saboteur/reporter/terminal.rb +11 -0
- data/lib/saboteur/result.rb +16 -5
- data/lib/saboteur/version.rb +1 -1
- data/lib/saboteur.rb +3 -0
- metadata +15 -2
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Saboteur
|
|
8
|
+
module Reporter
|
|
9
|
+
# One self-contained HTML page: inline style, no script, no network. It is
|
|
10
|
+
# rendered from the same hash the JSON reporter writes (Stryker.build), so
|
|
11
|
+
# both reports list the same mutants with the same statuses. The scores can
|
|
12
|
+
# differ: the JSON schema counts NoCoverage as undetected, while this page
|
|
13
|
+
# shows Report#score, which leaves it out (see the "Covered mutation score"
|
|
14
|
+
# label on the page). The score note says what is and is not counted.
|
|
15
|
+
class Html
|
|
16
|
+
STYLE = <<~CSS
|
|
17
|
+
body{font:14px/1.5 system-ui,sans-serif;margin:1rem auto;max-width:72rem;padding:0 1rem;color:#1b1b1b}
|
|
18
|
+
h1,h2{margin:1.2em 0 .4em}table{border-collapse:collapse;width:100%;min-width:60rem}
|
|
19
|
+
th,td{border-bottom:1px solid #ddd;padding:.25rem .5rem;text-align:left;vertical-align:top;overflow-wrap:break-word}
|
|
20
|
+
td.fit{white-space:nowrap}td.wide{min-width:14rem}
|
|
21
|
+
th{background:#f4f4f4;font-size:12px;text-transform:uppercase}
|
|
22
|
+
.scroll{overflow-x:auto}.note{color:#555;font-size:12px}
|
|
23
|
+
.status{border-radius:3px;padding:0 .4rem;color:#fff;white-space:nowrap}
|
|
24
|
+
.Killed{background:#2e7d32}.Survived{background:#c62828}.Timeout{background:#b45309}
|
|
25
|
+
.RuntimeError{background:#6a1b9a}.NoCoverage{background:#455a64}.Ignored{background:#616161}
|
|
26
|
+
.Accepted{background:#8d6e63}
|
|
27
|
+
.warning{background:#fff8e1;border-left:4px solid #f9a825;margin:.3rem 0;padding:.3rem .6rem;overflow-wrap:anywhere}
|
|
28
|
+
code,pre{font-family:ui-monospace,monospace;font-size:13px}
|
|
29
|
+
pre{display:inline-block;vertical-align:top;margin:0;white-space:pre-wrap;overflow-wrap:anywhere}summary{cursor:pointer;font-weight:600;overflow-wrap:anywhere}
|
|
30
|
+
CSS
|
|
31
|
+
|
|
32
|
+
SCORE_FORMULA = "(killed + timeout) / (killed + survived + timeout)"
|
|
33
|
+
|
|
34
|
+
def initialize(path)
|
|
35
|
+
@path = path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call(report)
|
|
39
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
40
|
+
File.write(@path, self.class.build(report))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.build(report)
|
|
44
|
+
new(nil).build(report)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build(report)
|
|
48
|
+
data = Stryker.build(report)
|
|
49
|
+
names = test_names(data)
|
|
50
|
+
results = report.results.to_h do |result|
|
|
51
|
+
[[Stryker.relative(result.subject.file, report.root), result.digest], result]
|
|
52
|
+
end
|
|
53
|
+
<<~HTML
|
|
54
|
+
<!DOCTYPE html>
|
|
55
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
56
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
57
|
+
<title>saboteur report</title>
|
|
58
|
+
<style>#{STYLE}</style></head><body>
|
|
59
|
+
<h1>saboteur report</h1>
|
|
60
|
+
#{summary(report, data)}#{warnings(data[:warnings])}#{skipped_subjects(data[:skippedSubjects])}
|
|
61
|
+
<h2>Files</h2>
|
|
62
|
+
#{data[:files].map { |file, entry| file_section(file, entry, names, results) }.join("\n")}
|
|
63
|
+
</body></html>
|
|
64
|
+
HTML
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def summary(report, data)
|
|
70
|
+
seed = data.dig(:config, :seed)
|
|
71
|
+
seed_note = seed ? ", seed #{seed}" : ""
|
|
72
|
+
counts = data[:files].values.flat_map { |entry| entry[:mutants] }.map { |mutant| mutant[:status] }.tally
|
|
73
|
+
<<~HTML
|
|
74
|
+
<p><strong>Covered mutation score: #{score_text(report.score)}</strong>
|
|
75
|
+
(#{report.subjects.size} subjects, #{report.results.size} mutants, #{format('%.1f', report.wall_time)}s#{seed_note})</p>
|
|
76
|
+
<p class="note">Covered mutation score = #{SCORE_FORMULA}. NoCoverage, errors, resource limits and skipped
|
|
77
|
+
mutants are not counted. Survivors accepted on the ignore list stay in the denominator. It covers only the subjects of this run, not the whole project. A tool that reads the
|
|
78
|
+
Stryker JSON may show a different score, because the schema counts NoCoverage as undetected.</p>
|
|
79
|
+
<p class="note">Wide tables scroll sideways.</p>
|
|
80
|
+
<p>#{counts.map { |status, count| "#{status_label(status)} #{count}" }.join(' ')}</p>
|
|
81
|
+
<p>Survivors: #{report.unignored_survivors.size} to fix, #{report.survivors.size - report.unignored_survivors.size}
|
|
82
|
+
accepted on the ignore list (still shown as Survived, with the reason).</p>
|
|
83
|
+
HTML
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def score_text(score)
|
|
87
|
+
score ? format("%.1f%%", score * 100) : "n/a"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def warnings(messages)
|
|
91
|
+
return "" if messages.empty?
|
|
92
|
+
|
|
93
|
+
items = messages.map { |message| %(<div class="warning">#{escape(message)}</div>) }
|
|
94
|
+
"<h2>Warnings</h2>\n#{items.join("\n")}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def skipped_subjects(entries)
|
|
98
|
+
return "" if entries.empty?
|
|
99
|
+
|
|
100
|
+
rows = entries.map do |entry|
|
|
101
|
+
"<tr><td>#{escape(entry[:subject])}</td><td>#{escape(entry[:file])}:#{entry[:line]}</td>" \
|
|
102
|
+
"<td>#{escape(entry[:reason])}</td></tr>"
|
|
103
|
+
end
|
|
104
|
+
"<h2>Skipped subjects</h2>\n<div class=\"scroll\"><table><thead><tr><th>Subject</th><th>Location</th>" \
|
|
105
|
+
"<th>Reason</th></tr></thead>#{rows.join}</table></div>"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_names(data)
|
|
109
|
+
data[:testFiles].values.flat_map { |group| group[:tests] }.to_h { |test| [test[:id], test[:name]] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def file_section(file, entry, names, results)
|
|
113
|
+
open = entry[:mutants].any? { |mutant| mutant[:status] == "Survived" } ? " open" : ""
|
|
114
|
+
ordered = entry[:mutants].sort_by { |mutant| position_key(mutant) }
|
|
115
|
+
rows = ordered.map { |mutant| mutant_row(mutant, names, results[[file, mutant[:id]]]) }
|
|
116
|
+
"<details#{open}><summary>#{escape(file)} (#{entry[:mutants].size} mutants)</summary>\n" \
|
|
117
|
+
"<div class=\"scroll\"><table><thead><tr><th>Location</th><th>Status</th><th>Operator</th><th>Change</th>" \
|
|
118
|
+
"<th>Id</th><th>Notes</th></tr></thead>\n#{rows.join("\n")}</table></div></details>"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def position_key(mutant)
|
|
122
|
+
start = mutant[:location][:start]
|
|
123
|
+
[start[:line], start[:column], mutant[:id]]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def mutant_row(mutant, names, result)
|
|
127
|
+
start = mutant[:location][:start]
|
|
128
|
+
tests = Array(mutant[:killedBy]).map { |id| names.fetch(id) }.join(", ")
|
|
129
|
+
notes = [mutant[:statusReason], tests.empty? ? nil : "killed by #{tests}"].compact.join("; ")
|
|
130
|
+
"<tr><td class=\"fit\">line #{start[:line]}, column #{start[:column]}</td><td>#{row_status(mutant,
|
|
131
|
+
result)}</td>" \
|
|
132
|
+
"<td>#{escape(mutant[:mutatorName])}</td><td class=\"wide\">#{change(mutant, result)}</td>" \
|
|
133
|
+
"<td class=\"fit\"><code>#{escape(mutant[:id])}</code></td><td class=\"wide\">#{escape(notes)}</td></tr>"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def row_status(mutant, result)
|
|
137
|
+
badge = status_label(mutant[:status])
|
|
138
|
+
# Only survivors are ever put on the ignore list.
|
|
139
|
+
result.ignored ? "#{badge} #{status_label('Accepted')}" : badge
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# One edit shows the original text and its replacement. A mutant with
|
|
143
|
+
# several edits shows only the replacements, because one location cannot
|
|
144
|
+
# stand for all of them.
|
|
145
|
+
def change(mutant, result)
|
|
146
|
+
edits = result.mutation.edits
|
|
147
|
+
return "<pre>#{escape(mutant[:replacement])}</pre>" unless edits.size == 1
|
|
148
|
+
|
|
149
|
+
source = result.subject.file_source
|
|
150
|
+
edit = edits.first
|
|
151
|
+
original = source.byteslice(edit.start_byte, edit.end_byte - edit.start_byte).to_s
|
|
152
|
+
"<pre>#{escape(original.dup.force_encoding(Encoding::UTF_8).scrub)}</pre> becomes " \
|
|
153
|
+
"<pre>#{escape(edit.replacement)}</pre>"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def status_label(status)
|
|
157
|
+
%(<span class="status #{status}">#{status}</span>)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def escape(text)
|
|
161
|
+
CGI.escapeHTML(text.to_s)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Saboteur
|
|
5
|
+
module Reporter
|
|
6
|
+
# A replay is an inspection too, regardless of the resulting verdict.
|
|
7
|
+
class Replay
|
|
8
|
+
def initialize(out)
|
|
9
|
+
@out = out
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(report)
|
|
13
|
+
report.results.each do |result|
|
|
14
|
+
@out.puts "\nReplay: #{result.subject.id} #{result.mutation.operator} #{result.digest} (#{result.status})"
|
|
15
|
+
@out.puts result.diff(root: report.root)
|
|
16
|
+
@out.puts "Selected tests:"
|
|
17
|
+
result.covered_by.each { |name| @out.puts " #{name}" }
|
|
18
|
+
@out.puts " none" if result.covered_by.empty?
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -34,10 +34,20 @@ module Saboteur
|
|
|
34
34
|
projectRoot: report.root,
|
|
35
35
|
files: files,
|
|
36
36
|
testFiles: test_files(test_ids),
|
|
37
|
-
|
|
37
|
+
skippedSubjects: skipped_subjects(report),
|
|
38
|
+
rejectedKills: report.rejected_kills,
|
|
39
|
+
warnings: report.warnings,
|
|
40
|
+
framework: { name: "saboteur", version: VERSION },
|
|
41
|
+
saboteur: { findings: Findings.build(report) }
|
|
38
42
|
}.merge(report.seed ? { config: { seed: report.seed } } : {})
|
|
39
43
|
end
|
|
40
44
|
|
|
45
|
+
def self.skipped_subjects(report)
|
|
46
|
+
report.skipped_subjects.map do |subject, reason|
|
|
47
|
+
{ subject: subject.id, file: relative(subject.file, report.root), line: subject.start_line, reason: reason }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
41
51
|
def self.test_index(results)
|
|
42
52
|
names = results.flat_map { |result| result.covered_by + result.killed_by }.uniq
|
|
43
53
|
names.each_with_index.to_h { |name, index| [name, index.to_s] }
|
|
@@ -14,6 +14,7 @@ module Saboteur
|
|
|
14
14
|
|
|
15
15
|
def call(report)
|
|
16
16
|
summary(report)
|
|
17
|
+
skipped_subjects(report)
|
|
17
18
|
warnings(report)
|
|
18
19
|
errors(report)
|
|
19
20
|
survivors(report)
|
|
@@ -50,6 +51,7 @@ module Saboteur
|
|
|
50
51
|
reused = report.results.count(&:reused?)
|
|
51
52
|
@out.puts "reused #{reused} of #{report.results.size} verdicts from earlier runs (--incremental)" if
|
|
52
53
|
reused.positive?
|
|
54
|
+
@out.puts "kills rejected by unchanged-method check: #{report.rejected_kills}"
|
|
53
55
|
kill_causes(report)
|
|
54
56
|
end
|
|
55
57
|
|
|
@@ -80,6 +82,15 @@ module Saboteur
|
|
|
80
82
|
parts.join(", ")
|
|
81
83
|
end
|
|
82
84
|
|
|
85
|
+
def skipped_subjects(report)
|
|
86
|
+
report.skipped_subjects.values.tally.each do |reason, count|
|
|
87
|
+
@out.puts "skipped subjects: #{reason}: #{count}"
|
|
88
|
+
end
|
|
89
|
+
report.skipped_subjects.each do |subject, reason|
|
|
90
|
+
@out.puts " #{subject.id} #{relative(subject.file, report.root)}:#{subject.start_line}: #{reason}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
83
94
|
def warnings(report)
|
|
84
95
|
report.warnings.each { |warning| @out.puts "warning: #{warning}" }
|
|
85
96
|
end
|
data/lib/saboteur/result.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Saboteur
|
|
|
17
17
|
attr_accessor :ignored
|
|
18
18
|
|
|
19
19
|
def initialize(subject:, mutation:, status:, mutated_source:, reason: nil, runtime: nil, killed_by: [],
|
|
20
|
-
covered_by: [], kill_cause: nil, reused: false)
|
|
20
|
+
covered_by: [], kill_cause: nil, reused: false, rejected_kill: false)
|
|
21
21
|
raise ArgumentError, "unknown status #{status.inspect}" unless STATUSES.include?(status)
|
|
22
22
|
|
|
23
23
|
@subject = subject
|
|
@@ -29,14 +29,20 @@ module Saboteur
|
|
|
29
29
|
@covered_by = covered_by
|
|
30
30
|
@mutated_source = mutated_source
|
|
31
31
|
@kill_cause = kill_cause
|
|
32
|
+
@rejected_kill = rejected_kill
|
|
32
33
|
@reused = reused
|
|
33
34
|
@ignored = nil
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# The same mutant, marked as an error: its verdict did not hold up.
|
|
37
|
-
def as_error(reason)
|
|
38
|
+
def as_error(reason, rejected_kill: false)
|
|
38
39
|
MutantResult.new(subject: subject, mutation: mutation, status: :error, mutated_source: mutated_source,
|
|
39
|
-
reason: reason, runtime: runtime, killed_by: killed_by, covered_by: covered_by
|
|
40
|
+
reason: reason, runtime: runtime, killed_by: killed_by, covered_by: covered_by,
|
|
41
|
+
rejected_kill: rejected_kill)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def rejected_kill?
|
|
45
|
+
@rejected_kill
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def digest
|
|
@@ -78,12 +84,12 @@ module Saboteur
|
|
|
78
84
|
|
|
79
85
|
# Everything one run produced.
|
|
80
86
|
class Report
|
|
81
|
-
attr_reader :results, :subjects, :wall_time, :invalid_mutations, :warnings, :root, :seed
|
|
87
|
+
attr_reader :results, :subjects, :wall_time, :invalid_mutations, :warnings, :root, :seed, :skipped_subjects
|
|
82
88
|
|
|
83
89
|
# REVIEW: diff mode; the terminal then shows one survivor per line.
|
|
84
90
|
# seed: the random seed every test run started from (nil when unknown).
|
|
85
91
|
def initialize(results:, subjects:, wall_time:, root:, invalid_mutations: 0, warnings: [], review: false,
|
|
86
|
-
seed: nil)
|
|
92
|
+
seed: nil, skipped_subjects: {})
|
|
87
93
|
@results = results
|
|
88
94
|
@subjects = subjects
|
|
89
95
|
@wall_time = wall_time
|
|
@@ -92,6 +98,11 @@ module Saboteur
|
|
|
92
98
|
@warnings = warnings
|
|
93
99
|
@review = review
|
|
94
100
|
@seed = seed
|
|
101
|
+
@skipped_subjects = skipped_subjects
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def rejected_kills
|
|
105
|
+
results.count(&:rejected_kill?)
|
|
95
106
|
end
|
|
96
107
|
|
|
97
108
|
def review?
|
data/lib/saboteur/version.rb
CHANGED
data/lib/saboteur.rb
CHANGED
|
@@ -38,6 +38,7 @@ require_relative "saboteur/execution/supervisor"
|
|
|
38
38
|
require_relative "saboteur/execution/reevaluator"
|
|
39
39
|
require_relative "saboteur/execution/runner"
|
|
40
40
|
require_relative "saboteur/execution/worker_pool"
|
|
41
|
+
require_relative "saboteur/execution/fork_readiness"
|
|
41
42
|
require_relative "saboteur/selection/coverage_map"
|
|
42
43
|
require_relative "saboteur/selection/recorder"
|
|
43
44
|
require_relative "saboteur/selection/git_diff"
|
|
@@ -46,6 +47,8 @@ require_relative "saboteur/config"
|
|
|
46
47
|
require_relative "saboteur/ignore_list"
|
|
47
48
|
require_relative "saboteur/frameworks/rails"
|
|
48
49
|
require_relative "saboteur/reporter/diff"
|
|
50
|
+
require_relative "saboteur/reporter/findings"
|
|
49
51
|
require_relative "saboteur/reporter/terminal"
|
|
50
52
|
require_relative "saboteur/reporter/stryker"
|
|
53
|
+
require_relative "saboteur/reporter/html"
|
|
51
54
|
require_relative "saboteur/engine"
|
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.
|
|
4
|
+
version: 0.3.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-
|
|
11
|
+
date: 2026-10-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: fiddle
|
|
@@ -59,20 +59,26 @@ files:
|
|
|
59
59
|
- README.md
|
|
60
60
|
- exe/saboteur
|
|
61
61
|
- lib/saboteur.rb
|
|
62
|
+
- lib/saboteur/baseline_health.rb
|
|
62
63
|
- lib/saboteur/cli.rb
|
|
63
64
|
- lib/saboteur/config.rb
|
|
65
|
+
- lib/saboteur/doctor.rb
|
|
64
66
|
- lib/saboteur/engine.rb
|
|
67
|
+
- lib/saboteur/engine/baseline_guard.rb
|
|
65
68
|
- lib/saboteur/engine/boot.rb
|
|
69
|
+
- lib/saboteur/engine/candidate_selection.rb
|
|
66
70
|
- lib/saboteur/engine/candidates.rb
|
|
67
71
|
- lib/saboteur/engine/coverage_store.rb
|
|
68
72
|
- lib/saboteur/engine/diagnostics.rb
|
|
69
73
|
- lib/saboteur/engine/diff_scope.rb
|
|
70
74
|
- lib/saboteur/engine/distribution.rb
|
|
71
75
|
- lib/saboteur/engine/result_cache.rb
|
|
76
|
+
- lib/saboteur/engine/run_fingerprint.rb
|
|
72
77
|
- lib/saboteur/engine/scheduler.rb
|
|
73
78
|
- lib/saboteur/engine/subject_selection.rb
|
|
74
79
|
- lib/saboteur/engine/suite_files.rb
|
|
75
80
|
- lib/saboteur/engine/verdict_check.rb
|
|
81
|
+
- lib/saboteur/execution/fork_readiness.rb
|
|
76
82
|
- lib/saboteur/execution/process_table.rb
|
|
77
83
|
- lib/saboteur/execution/process_tree.rb
|
|
78
84
|
- lib/saboteur/execution/protocol.rb
|
|
@@ -81,6 +87,7 @@ files:
|
|
|
81
87
|
- lib/saboteur/execution/supervisor.rb
|
|
82
88
|
- lib/saboteur/execution/worker_pool.rb
|
|
83
89
|
- lib/saboteur/frameworks/rails.rb
|
|
90
|
+
- lib/saboteur/help.rb
|
|
84
91
|
- lib/saboteur/hooks.rb
|
|
85
92
|
- lib/saboteur/ignore_list.rb
|
|
86
93
|
- lib/saboteur/integration/base.rb
|
|
@@ -88,8 +95,14 @@ files:
|
|
|
88
95
|
- lib/saboteur/mutation_source/base.rb
|
|
89
96
|
- lib/saboteur/mutation_source/builtin.rb
|
|
90
97
|
- lib/saboteur/mutation_source/edit.rb
|
|
98
|
+
- lib/saboteur/mutation_source/filter_replacement.rb
|
|
99
|
+
- lib/saboteur/mutation_source/literal_replacement.rb
|
|
91
100
|
- lib/saboteur/mutation_source/mutation.rb
|
|
101
|
+
- lib/saboteur/replay.rb
|
|
92
102
|
- lib/saboteur/reporter/diff.rb
|
|
103
|
+
- lib/saboteur/reporter/findings.rb
|
|
104
|
+
- lib/saboteur/reporter/html.rb
|
|
105
|
+
- lib/saboteur/reporter/replay.rb
|
|
93
106
|
- lib/saboteur/reporter/stryker.rb
|
|
94
107
|
- lib/saboteur/reporter/terminal.rb
|
|
95
108
|
- lib/saboteur/result.rb
|