saboteur 0.11.1 → 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 +35 -0
- data/README.md +1 -1
- data/lib/saboteur/cli/exit_status.rb +6 -2
- data/lib/saboteur/cli/explain_command.rb +51 -0
- data/lib/saboteur/cli/flaky_exit.rb +23 -0
- data/lib/saboteur/cli/run_command.rb +13 -0
- 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 +1 -1
- data/lib/saboteur/cli.rb +3 -0
- data/lib/saboteur/config.rb +7 -2
- data/lib/saboteur/doctor.rb +8 -1
- 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/scheduler.rb +1 -1
- data/lib/saboteur/engine/selection_announcement.rb +5 -4
- data/lib/saboteur/engine/subject_selection.rb +10 -0
- data/lib/saboteur/engine.rb +35 -5
- data/lib/saboteur/help.rb +11 -0
- data/lib/saboteur/ignore_list.rb +22 -2
- data/lib/saboteur/mutation_source/builtin.rb +1 -0
- data/lib/saboteur/mutation_source/operator_explain.rb +181 -0
- data/lib/saboteur/reporter/agent_commands.rb +23 -6
- data/lib/saboteur/reporter/agent_incomplete.rb +9 -2
- data/lib/saboteur/reporter/agent_summary.rb +2 -1
- data/lib/saboteur/reporter/gap_kinds.rb +80 -0
- data/lib/saboteur/reporter/html.rb +19 -4
- data/lib/saboteur/reporter/review_comments.rb +6 -5
- data/lib/saboteur/reporter/score_table.rb +8 -7
- 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 +53 -3
- data/lib/saboteur/reporter/terminal.rb +27 -29
- data/lib/saboteur/reporter/wording.rb +37 -0
- data/lib/saboteur/result.rb +10 -0
- data/lib/saboteur/text.rb +6 -0
- data/lib/saboteur/version.rb +1 -1
- metadata +6 -1
|
@@ -19,6 +19,11 @@ module Saboteur
|
|
|
19
19
|
# --all-mutants gives every survivor its own block. Accepted
|
|
20
20
|
# (ignored) survivors get one quoted dim line each, never a block.
|
|
21
21
|
class SurvivorList
|
|
22
|
+
# Default when a caller never wires the whole-suite lookup (unit pins,
|
|
23
|
+
# other renderers): keep the historical bare covered-by line. Terminal
|
|
24
|
+
# always passes the kwarg - nil there means the lookup ran and found
|
|
25
|
+
# no cached whole-suite map (review C1).
|
|
26
|
+
NO_WHOLE_SUITE_LOOKUP = Object.new
|
|
22
27
|
# Rendered rows in the method fence, elision markers included.
|
|
23
28
|
FENCE_CAP = 10
|
|
24
29
|
|
|
@@ -28,11 +33,14 @@ module Saboteur
|
|
|
28
33
|
# Sibling digests named on the collapse line; the rest are a count.
|
|
29
34
|
SIBLING_DIGEST_CAP = 3
|
|
30
35
|
|
|
31
|
-
def initialize(out, palette, all_mutants: false, classifier: SurvivorClassification
|
|
36
|
+
def initialize(out, palette, all_mutants: false, classifier: SurvivorClassification,
|
|
37
|
+
whole_suite_map: NO_WHOLE_SUITE_LOOKUP, run_map: nil)
|
|
32
38
|
@out = out
|
|
33
39
|
@palette = palette
|
|
34
40
|
@all_mutants = all_mutants
|
|
35
41
|
@classifier = classifier
|
|
42
|
+
@whole_suite_map = whole_suite_map
|
|
43
|
+
@run_map = run_map
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
# Survivors in report (ranked) order; accepted ones trail as dim lines.
|
|
@@ -41,7 +49,8 @@ module Saboteur
|
|
|
41
49
|
|
|
42
50
|
live, accepted = survivors.partition(&:unignored_survivor?)
|
|
43
51
|
@out.puts @palette.paint(:head, "Survivors (#{live.size}) - changes your tests did not catch")
|
|
44
|
-
|
|
52
|
+
block_list = groups(live)
|
|
53
|
+
block_list.each_with_index { |group, index| block(report, group, index + 1, block_list.size) }
|
|
45
54
|
accepted_section(report, accepted)
|
|
46
55
|
hinted = live.select { |result| hint_for(result) }
|
|
47
56
|
@out.puts "\n#{@palette.paint(:dim, SurvivorHints::CAVEAT)}" if hinted.any?
|
|
@@ -72,9 +81,12 @@ module Saboteur
|
|
|
72
81
|
siblings(report, group)
|
|
73
82
|
end
|
|
74
83
|
|
|
84
|
+
# The count names blocks, the unit the leading ordinal uses: grouped
|
|
85
|
+
# siblings read "#3 of 14 blocks", never blocks against mutants (#1147).
|
|
75
86
|
def label_suffix(result, rank, total)
|
|
76
87
|
label = @classifier.label(result)
|
|
77
|
-
|
|
88
|
+
unit = total == 1 ? "block" : "blocks"
|
|
89
|
+
parts = [("coarse" if result.mutation.coarse), label, "##{rank} of #{total} #{unit}"].compact
|
|
78
90
|
reason = SurvivorHints.label_reason(label)
|
|
79
91
|
"[#{parts.join(" \u00B7 ")}#{": #{reason}" if reason}]"
|
|
80
92
|
end
|
|
@@ -178,8 +190,38 @@ module Saboteur
|
|
|
178
190
|
|
|
179
191
|
# The covering tests are a group union (#502): the first names where an
|
|
180
192
|
# assertion would pin the change, the count says the rest also passed.
|
|
193
|
+
#
|
|
194
|
+
# The list comes from the run's own map, which a scoped run narrows to
|
|
195
|
+
# the specs it loaded. The cached whole-suite map (when one exists)
|
|
196
|
+
# tells the rest of the story (review C1): a covering spec the run never
|
|
197
|
+
# loaded is named as not run here, and a spec pinning the mutant's exact
|
|
198
|
+
# line marks the block as likely covered elsewhere, instead of reading
|
|
199
|
+
# as a bare finding. With no whole-suite map, say so rather than
|
|
200
|
+
# implying the list is complete. A whole-project run's own map IS the
|
|
201
|
+
# whole-suite map, so its line is unchanged.
|
|
181
202
|
def covered(group)
|
|
182
203
|
tests = group.flat_map(&:covered_by).uniq
|
|
204
|
+
return legacy_covered(group, tests) if @whole_suite_map.equal?(NO_WHOLE_SUITE_LOOKUP)
|
|
205
|
+
|
|
206
|
+
whole = @whole_suite_map&.tests_for(group.first.subject)
|
|
207
|
+
if whole.nil?
|
|
208
|
+
@out.puts @palette.paint(:dim, " covering tests unknown - run a full coverage build")
|
|
209
|
+
return
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
run_known = @run_map ? @run_map.tests_for(group.first.subject) : tests
|
|
213
|
+
not_run = whole - run_known
|
|
214
|
+
return legacy_covered(group, tests) if not_run.empty?
|
|
215
|
+
|
|
216
|
+
@out.puts " covered by #{whole.size} #{whole.one? ? 'spec' : 'specs'}, " \
|
|
217
|
+
"incl. #{not_run.first} (not run here)"
|
|
218
|
+
exact = exact_line_tests(group.first) - run_known
|
|
219
|
+
return if exact.empty?
|
|
220
|
+
|
|
221
|
+
@out.puts @palette.paint(:dim, " likely covered elsewhere: #{exact.first} pins this line (not run here)")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def legacy_covered(group, tests)
|
|
183
225
|
return if tests.empty?
|
|
184
226
|
|
|
185
227
|
first = group.find { |result| result.covered_by.include?(tests.first) }
|
|
@@ -188,6 +230,14 @@ module Saboteur
|
|
|
188
230
|
@out.puts @palette.paint(:dim, " #{line}") if line
|
|
189
231
|
end
|
|
190
232
|
|
|
233
|
+
# Tests pinning the mutant's exact edit lines in the whole-suite map,
|
|
234
|
+
# on the same line basis the scheduler used to select the run's tests:
|
|
235
|
+
# the strongest "already pinned elsewhere" evidence a map can give.
|
|
236
|
+
def exact_line_tests(result)
|
|
237
|
+
lines = result.mutation.edits.flat_map { |edit| result.subject.coverage_lines_for(edit) }.uniq
|
|
238
|
+
@whole_suite_map.tests_for_edit(result.subject, lines)
|
|
239
|
+
end
|
|
240
|
+
|
|
191
241
|
# The verdict names what this run did: executed is nil when the run
|
|
192
242
|
# did not record which tests ran, so only the bare verdict is safe.
|
|
193
243
|
def more_covered_line(group, tests)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require_relative "safe_out"
|
|
5
|
+
require_relative "wording"
|
|
5
6
|
require_relative "baseline_tail"
|
|
6
7
|
require_relative "next_steps"
|
|
7
8
|
require_relative "kill_causes"
|
|
@@ -27,17 +28,19 @@ module Saboteur
|
|
|
27
28
|
def initialize(out, verbose: false, quiet: false, color: :auto, overrides: nil,
|
|
28
29
|
replay_command: nil, all_mutants: false, exit_code: nil,
|
|
29
30
|
threshold: nil, previous_score: nil, baseline_gate: nil,
|
|
30
|
-
scope: "whole project",
|
|
31
|
+
scope: "whole project", baseline: nil, coverage: nil, mutant_phase: nil,
|
|
32
|
+
whole_suite_map: SurvivorList::NO_WHOLE_SUITE_LOOKUP, run_map: nil)
|
|
31
33
|
@out = SafeOut.new(out)
|
|
32
34
|
@all_mutants = all_mutants
|
|
33
35
|
@verbose = verbose
|
|
34
36
|
@quiet = quiet
|
|
35
|
-
# Clean-screen context: scope
|
|
37
|
+
# Clean-screen context: scope from the CLI; PR4a plumbs the phase stats.
|
|
36
38
|
@scope = scope
|
|
37
|
-
@html_report = html_report
|
|
38
39
|
@baseline = baseline
|
|
39
40
|
@coverage = coverage
|
|
40
41
|
@mutant_phase = mutant_phase
|
|
42
|
+
@whole_suite_map = whole_suite_map
|
|
43
|
+
@run_map = run_map
|
|
41
44
|
@palette = Palette.for(out, mode: color, overrides: overrides)
|
|
42
45
|
@score_table = ScoreTable.new(@out, @palette)
|
|
43
46
|
@next_steps = NextSteps.new(@out, @palette, replay_command: replay_command)
|
|
@@ -143,9 +146,9 @@ module Saboteur
|
|
|
143
146
|
# numbers so "4 mutants" is not read as the whole plan (#582).
|
|
144
147
|
def mutant_counts(report)
|
|
145
148
|
planned = report.early_stop&.planned_mutants
|
|
146
|
-
return
|
|
149
|
+
return Text.counted(report.results.size, "mutant") unless planned && planned > report.results.size
|
|
147
150
|
|
|
148
|
-
"#{planned}
|
|
151
|
+
"#{Text.counted(planned, 'mutant')} selected, #{report.results.size} attempted"
|
|
149
152
|
end
|
|
150
153
|
|
|
151
154
|
def summary(report)
|
|
@@ -153,7 +156,7 @@ module Saboteur
|
|
|
153
156
|
return clean_screen(report) if !@baseline_gate && clean_run?(report)
|
|
154
157
|
|
|
155
158
|
seed = report.seed && ", seed #{report.seed} (repeat with --seed #{report.seed})"
|
|
156
|
-
tally = "saboteur #{VERSION}: #{report.subjects.size}
|
|
159
|
+
tally = "saboteur #{VERSION}: #{Text.counted(report.subjects.size, 'subject')}, #{mutant_counts(report)}, " \
|
|
157
160
|
"wall #{format('%.2f', report.wall_time)} s#{seed}"
|
|
158
161
|
@out.puts @palette.paint(:dim, tally)
|
|
159
162
|
@out.puts report.shard.line if report.shard
|
|
@@ -185,9 +188,11 @@ module Saboteur
|
|
|
185
188
|
total = @palette.paint(:dim, "#{middot} #{format('%.1f', report.wall_time)}s total")
|
|
186
189
|
@out.puts "#{@palette.paint(:pass, win)} #{total}"
|
|
187
190
|
untested = untested_subjects(report)
|
|
188
|
-
@out.puts " #{@palette.paint(:warn, untested_win_note(untested))}" if untested.any?
|
|
191
|
+
@out.puts " #{@palette.paint(:warn, Wording.untested_win_note(untested))}" if untested.any?
|
|
189
192
|
ReuseNote.new(@out).print(report)
|
|
190
|
-
|
|
193
|
+
# The footer owns the report paths (#1187); the clean screen's own
|
|
194
|
+
# hint is the follow-up run a whole-project win suggests.
|
|
195
|
+
@out.puts " #{@palette.paint(:dim, 'next: saboteur run --since origin/main')}" if @scope == "whole project"
|
|
191
196
|
end
|
|
192
197
|
|
|
193
198
|
# The phase lines and the progress-dots line print only when their data
|
|
@@ -209,19 +214,6 @@ module Saboteur
|
|
|
209
214
|
"#{report.counts[:killed]}/#{report.executed.size} mutants killed"
|
|
210
215
|
end
|
|
211
216
|
|
|
212
|
-
def untested_win_note(untested)
|
|
213
|
-
ids = untested.map(&:id).join(", ")
|
|
214
|
-
"#{untested.size} subject#{'s' unless untested.one?} not scored - no tests exist: #{ids}"
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def clean_reports_line
|
|
218
|
-
return unless @html_report
|
|
219
|
-
|
|
220
|
-
line = "reports: #{@html_report}"
|
|
221
|
-
line += " #{middot} next: saboteur run --since origin/main" if @scope == "whole project"
|
|
222
|
-
@out.puts " #{@palette.paint(:dim, line)}"
|
|
223
|
-
end
|
|
224
|
-
|
|
225
217
|
def hazard_suppressions(report)
|
|
226
218
|
return unless report.hazard_suppressions.positive?
|
|
227
219
|
|
|
@@ -250,22 +242,26 @@ module Saboteur
|
|
|
250
242
|
end
|
|
251
243
|
|
|
252
244
|
line = "Score: #{painted_score(breakdown.value)}#{headline_context(breakdown.value)}"
|
|
253
|
-
line <<
|
|
254
|
-
" - nothing tested"
|
|
255
|
-
else
|
|
256
|
-
" (#{breakdown.detected}/#{breakdown.tested} killed, tested subjects)"
|
|
257
|
-
end
|
|
245
|
+
line << score_parenthetical(breakdown)
|
|
258
246
|
if @verbose && breakdown.tested.positive?
|
|
259
247
|
line << "#{score_split(breakdown)} (killed+timeout)/(killed+survived+timeout); " \
|
|
260
248
|
"errors, resource limits and skips excluded"
|
|
261
249
|
end
|
|
262
250
|
if untested.any?
|
|
263
251
|
line << " #{@palette.paint(:warn, "#{untested.size} subject#{'s' unless untested.one?} " \
|
|
264
|
-
|
|
252
|
+
"not scored - #{Wording.no_execution(untested.size)}")}"
|
|
265
253
|
end
|
|
266
254
|
@out.puts line
|
|
267
255
|
end
|
|
268
256
|
|
|
257
|
+
# The share of tested mutants the run detected: "killed" alone would
|
|
258
|
+
# count timeouts too, so Wording names both when one is in the number.
|
|
259
|
+
def score_parenthetical(breakdown)
|
|
260
|
+
return " - nothing tested" if breakdown.tested.zero?
|
|
261
|
+
|
|
262
|
+
" (#{breakdown.detected}/#{breakdown.tested} #{Wording.detected_word(breakdown)}, tested subjects)"
|
|
263
|
+
end
|
|
264
|
+
|
|
269
265
|
# What the score is judged against and where it came from: the
|
|
270
266
|
# threshold it must meet, and the last score the history recorded.
|
|
271
267
|
def headline_context(value)
|
|
@@ -307,7 +303,7 @@ module Saboteur
|
|
|
307
303
|
return if untested.empty?
|
|
308
304
|
|
|
309
305
|
@out.puts
|
|
310
|
-
@out.puts @palette.paint(:warn, "Not scored - no
|
|
306
|
+
@out.puts @palette.paint(:warn, "Not scored - no test executes these (treat these as your biggest gaps):")
|
|
311
307
|
entries = untested.map { |subject| "#{subject.id} (#{subject.location(report.root)})" }
|
|
312
308
|
line = " #{@palette.paint(:warn, '!')} #{entries.first(MAX_NOT_SCORED).join(" #{middot} ")}"
|
|
313
309
|
line << " #{middot} ... #{entries.size - MAX_NOT_SCORED} more" if entries.size > MAX_NOT_SCORED
|
|
@@ -597,7 +593,9 @@ module Saboteur
|
|
|
597
593
|
end
|
|
598
594
|
|
|
599
595
|
def survivors(report)
|
|
600
|
-
SurvivorList.new(@out, @palette, all_mutants: @all_mutants
|
|
596
|
+
SurvivorList.new(@out, @palette, all_mutants: @all_mutants,
|
|
597
|
+
whole_suite_map: @whole_suite_map, run_map: @run_map)
|
|
598
|
+
.call(report, ranked_survivors(report))
|
|
601
599
|
@out.puts unless report.survivors.empty?
|
|
602
600
|
end
|
|
603
601
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Saboteur
|
|
5
|
+
module Reporter
|
|
6
|
+
# One wording for the score parenthetical and the not-scored note, so
|
|
7
|
+
# the terminal screen and the HTML page never drift (#1145, #1146).
|
|
8
|
+
module Wording
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# detected counts timeouts, the tally does not: name both when a
|
|
12
|
+
# timeout is in the number, or "killed" means two things in adjacent
|
|
13
|
+
# lines (#1146).
|
|
14
|
+
def detected_word(breakdown)
|
|
15
|
+
breakdown.timeouts.positive? ? "killed or timed out" : "killed"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# A subject with no coverage may have tests that stub it away: the
|
|
19
|
+
# honest claim is about execution, not existence (#1145).
|
|
20
|
+
def no_execution(untested_count)
|
|
21
|
+
untested_count == 1 ? "no test executes it" : "no test executes them"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The score table's per-file note fragment: count, subject noun and
|
|
25
|
+
# the execution claim in one shape (#1145).
|
|
26
|
+
def untested_note(count)
|
|
27
|
+
"#{count} subject#{'s' unless count == 1} - #{no_execution(count)}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The clean screen's one-line note for subjects no run scored.
|
|
31
|
+
def untested_win_note(untested)
|
|
32
|
+
ids = untested.map(&:id).join(", ")
|
|
33
|
+
"#{untested.size} subject#{'s' unless untested.one?} not scored - #{no_execution(untested.size)}: #{ids}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
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)
|
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,7 +1,7 @@
|
|
|
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
|
|
@@ -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
|
|
@@ -247,6 +251,7 @@ files:
|
|
|
247
251
|
- lib/saboteur/reporter/survivor_ranking.rb
|
|
248
252
|
- lib/saboteur/reporter/terminal.rb
|
|
249
253
|
- lib/saboteur/reporter/test_matrix.rb
|
|
254
|
+
- lib/saboteur/reporter/wording.rb
|
|
250
255
|
- lib/saboteur/result.rb
|
|
251
256
|
- lib/saboteur/score.rb
|
|
252
257
|
- lib/saboteur/selection/coverage_map.rb
|