mutante 0.1.0 → 0.2.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/lib/mutante/cli.rb +9 -2
- data/lib/mutante/line_analyzer.rb +4 -0
- data/lib/mutante/reporter.rb +170 -51
- data/lib/mutante/runner.rb +3 -2
- data/lib/mutante/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae5a33de8e678194912023676158d0fa89fcd36654799e479c82bc6659c09850
|
|
4
|
+
data.tar.gz: dba11952956c4ebce0745a598d10202d86599348231d983b9025df7be5549de7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39ce2ce83999ddbe1a8b7c32f38c653c8146fa9aa9df243e40ff9cff1cd61db6fd42a1dab74a51ea9d0889bb7fe03db9847f545e89a026357f42475a95a7a425
|
|
7
|
+
data.tar.gz: 58eebb255f2e36162970157fe57840ed74e81c4e89217683c812e18fbe6fea14af4bd4eccc490a4445cc81e776d63a01faf7f5b2069894f566dc9bb64da6d9b3
|
data/lib/mutante/cli.rb
CHANGED
|
@@ -13,13 +13,16 @@ module Mutante
|
|
|
13
13
|
|
|
14
14
|
def initialize(argv)
|
|
15
15
|
@argv = argv.dup
|
|
16
|
-
@options = { verbose: false, config: nil }
|
|
16
|
+
@options = { verbose: false, config: nil, output: "mutante_uncovered.txt" }
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def run
|
|
20
20
|
target = parse!
|
|
21
21
|
load_configuration
|
|
22
|
-
ok = Runner.new(
|
|
22
|
+
ok = Runner.new(
|
|
23
|
+
verbose: @options[:verbose],
|
|
24
|
+
output_path: @options[:output]
|
|
25
|
+
).call(target)
|
|
23
26
|
exit(ok ? 0 : 1)
|
|
24
27
|
end
|
|
25
28
|
|
|
@@ -37,6 +40,10 @@ module Mutante
|
|
|
37
40
|
@options[:verbose] = true
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
o.on("-o", "--output PATH", "Write uncovered lines to PATH (default: mutante_uncovered.txt)") do |v|
|
|
44
|
+
@options[:output] = v
|
|
45
|
+
end
|
|
46
|
+
|
|
40
47
|
o.on("--version", "Print version and exit") do
|
|
41
48
|
puts "mutante #{Mutante::VERSION}"
|
|
42
49
|
exit 0
|
|
@@ -14,6 +14,10 @@ module Mutante
|
|
|
14
14
|
@original_lines = File.readlines(path)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def total_lines
|
|
18
|
+
@original_lines.size
|
|
19
|
+
end
|
|
20
|
+
|
|
17
21
|
# Yields |line_number, line_text| for each line worth trying.
|
|
18
22
|
# Inside the block, the file on disk has that line commented out.
|
|
19
23
|
# After the block returns, the file is restored to its original form.
|
data/lib/mutante/reporter.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Mutante
|
|
|
17
17
|
|
|
18
18
|
CLEAR_LINE = "\e[2K\r".freeze
|
|
19
19
|
|
|
20
|
-
def initialize(io: $stdout, force_plain: false)
|
|
20
|
+
def initialize(io: $stdout, force_plain: false, output_path: "mutante_uncovered.txt")
|
|
21
21
|
@io = io
|
|
22
22
|
@flagged = []
|
|
23
23
|
@tty = !force_plain && io.respond_to?(:tty?) && io.tty?
|
|
@@ -28,6 +28,11 @@ module Mutante
|
|
|
28
28
|
@file_started = nil
|
|
29
29
|
@started_at = nil
|
|
30
30
|
@spinner_index = 0
|
|
31
|
+
@output_path = output_path
|
|
32
|
+
@total_source_lines = 0
|
|
33
|
+
@total_eligible = 0
|
|
34
|
+
@mutex = Mutex.new
|
|
35
|
+
@spinner_thread = nil
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def starting(files)
|
|
@@ -69,79 +74,96 @@ module Mutante
|
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
def file_starting(path)
|
|
72
|
-
@
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
@mutex.synchronize do
|
|
78
|
+
@file_index += 1
|
|
79
|
+
@current_file = path
|
|
80
|
+
@counts = { tested: 0, flagged: 0, skipped: 0 }
|
|
81
|
+
@file_started = monotonic_now
|
|
82
|
+
|
|
83
|
+
if @tty
|
|
84
|
+
@io.puts
|
|
85
|
+
@io.puts "#{color(:cyan)}▸#{color(:reset)} " \
|
|
86
|
+
"#{color(:bold)}#{path}#{color(:reset)} " \
|
|
87
|
+
"#{color(:gray)}[#{@file_index}/#{@total_files}]#{color(:reset)}"
|
|
88
|
+
redraw_status
|
|
89
|
+
else
|
|
90
|
+
@io.puts path
|
|
91
|
+
end
|
|
85
92
|
end
|
|
93
|
+
start_spinner_thread
|
|
86
94
|
end
|
|
87
95
|
|
|
88
96
|
def line_flagged(path, line_number, line_text)
|
|
89
|
-
@
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
@mutex.synchronize do
|
|
98
|
+
@flagged << { path: path, line: line_number, text: line_text }
|
|
99
|
+
@counts[:flagged] += 1 if @counts
|
|
100
|
+
@counts[:tested] += 1 if @counts
|
|
101
|
+
|
|
102
|
+
if @tty
|
|
103
|
+
@io.print CLEAR_LINE
|
|
104
|
+
@io.puts " #{color(:red)}⚑#{color(:reset)} " \
|
|
105
|
+
"#{color(:red)}#{path}:#{line_number}#{color(:reset)} " \
|
|
106
|
+
"#{color(:dim)}#{line_text.strip}#{color(:reset)}"
|
|
107
|
+
redraw_status
|
|
108
|
+
else
|
|
109
|
+
@io.puts " flagged #{path}:#{line_number} #{line_text.strip}"
|
|
110
|
+
end
|
|
101
111
|
end
|
|
102
112
|
end
|
|
103
113
|
|
|
104
114
|
def line_safe(_path, _line_number)
|
|
105
|
-
@
|
|
115
|
+
@mutex.synchronize do
|
|
116
|
+
@counts[:tested] += 1 if @counts
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
if @tty
|
|
119
|
+
redraw_status
|
|
120
|
+
else
|
|
121
|
+
@io.print "."
|
|
122
|
+
@io.flush
|
|
123
|
+
end
|
|
112
124
|
end
|
|
113
125
|
end
|
|
114
126
|
|
|
115
127
|
def line_skipped(_path, _line_number, _reason)
|
|
116
|
-
@
|
|
128
|
+
@mutex.synchronize do
|
|
129
|
+
@counts[:skipped] += 1 if @counts
|
|
117
130
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
if @tty
|
|
132
|
+
redraw_status
|
|
133
|
+
else
|
|
134
|
+
@io.print "-"
|
|
135
|
+
@io.flush
|
|
136
|
+
end
|
|
123
137
|
end
|
|
124
138
|
end
|
|
125
139
|
|
|
126
|
-
def file_finished
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
def file_finished(total_lines: nil)
|
|
141
|
+
stop_spinner_thread
|
|
142
|
+
@mutex.synchronize do
|
|
143
|
+
@total_source_lines += total_lines.to_i if total_lines
|
|
144
|
+
@total_eligible += @counts[:tested] if @counts
|
|
145
|
+
|
|
146
|
+
if @tty
|
|
147
|
+
@io.print CLEAR_LINE
|
|
148
|
+
elapsed = format_duration(monotonic_now - (@file_started || monotonic_now))
|
|
149
|
+
icon = @counts && @counts[:flagged].positive? ? "#{color(:yellow)}⚑#{color(:reset)}" : "#{color(:green)}✓#{color(:reset)}"
|
|
150
|
+
parts = []
|
|
151
|
+
parts << "#{@counts[:tested]} tested" if @counts
|
|
152
|
+
parts << "#{color(:red)}#{@counts[:flagged]} flagged#{color(:reset)}" if @counts && @counts[:flagged].positive?
|
|
153
|
+
parts << "#{@counts[:skipped]} skipped" if @counts && @counts[:skipped].positive?
|
|
154
|
+
parts << "#{color(:gray)}#{elapsed}#{color(:reset)}"
|
|
155
|
+
@io.puts " #{icon} #{parts.join(color(:gray) + ' · ' + color(:reset))}"
|
|
156
|
+
else
|
|
157
|
+
@io.puts
|
|
158
|
+
end
|
|
139
159
|
end
|
|
140
160
|
end
|
|
141
161
|
|
|
142
162
|
def finished
|
|
163
|
+
stop_spinner_thread
|
|
143
164
|
if @tty
|
|
144
165
|
draw_summary
|
|
166
|
+
draw_totals
|
|
145
167
|
else
|
|
146
168
|
@io.puts
|
|
147
169
|
@io.puts "=" * 60
|
|
@@ -154,7 +176,10 @@ module Mutante
|
|
|
154
176
|
@io.puts " #{entry[:path]}:#{entry[:line]} #{entry[:text].strip}"
|
|
155
177
|
end
|
|
156
178
|
end
|
|
179
|
+
print_totals_plain
|
|
157
180
|
end
|
|
181
|
+
|
|
182
|
+
write_uncovered_file
|
|
158
183
|
end
|
|
159
184
|
|
|
160
185
|
attr_reader :flagged
|
|
@@ -173,10 +198,13 @@ module Mutante
|
|
|
173
198
|
@spinner_index = (@spinner_index + 1) % SPINNER_FRAMES.size
|
|
174
199
|
spinner = SPINNER_FRAMES[@spinner_index]
|
|
175
200
|
|
|
201
|
+
elapsed = format_duration(monotonic_now - (@file_started || monotonic_now))
|
|
202
|
+
|
|
176
203
|
parts = []
|
|
177
204
|
parts << "#{@counts[:tested]} tested"
|
|
178
205
|
parts << "#{color(:red)}⚑ #{@counts[:flagged]}#{color(:reset)}" if @counts[:flagged].positive?
|
|
179
206
|
parts << "#{color(:gray)}#{@counts[:skipped]} skipped#{color(:reset)}" if @counts[:skipped].positive?
|
|
207
|
+
parts << "#{color(:gray)}#{elapsed}#{color(:reset)}"
|
|
180
208
|
|
|
181
209
|
line = " #{color(:cyan)}#{spinner}#{color(:reset)} " \
|
|
182
210
|
"#{parts.join(color(:gray) + ' · ' + color(:reset))}"
|
|
@@ -186,6 +214,27 @@ module Mutante
|
|
|
186
214
|
@io.flush
|
|
187
215
|
end
|
|
188
216
|
|
|
217
|
+
def start_spinner_thread
|
|
218
|
+
return unless @tty
|
|
219
|
+
stop_spinner_thread
|
|
220
|
+
|
|
221
|
+
@spinner_thread = Thread.new do
|
|
222
|
+
loop do
|
|
223
|
+
sleep 0.1
|
|
224
|
+
@mutex.synchronize { redraw_status }
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def stop_spinner_thread
|
|
230
|
+
t = @spinner_thread
|
|
231
|
+
return unless t
|
|
232
|
+
|
|
233
|
+
t.kill
|
|
234
|
+
t.join
|
|
235
|
+
@spinner_thread = nil
|
|
236
|
+
end
|
|
237
|
+
|
|
189
238
|
def draw_header(title, subtitle)
|
|
190
239
|
width = [title.length, subtitle.length].max + 4
|
|
191
240
|
top = "╭#{"─" * width}╮"
|
|
@@ -244,6 +293,76 @@ module Mutante
|
|
|
244
293
|
str.gsub(/\e\[[0-9;]*m/, "").length
|
|
245
294
|
end
|
|
246
295
|
|
|
296
|
+
def covered_count
|
|
297
|
+
@total_eligible - @flagged.size
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def uncovered_count
|
|
301
|
+
@flagged.size
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def percent(part, whole)
|
|
305
|
+
return "0.0%" if whole.nil? || whole.zero?
|
|
306
|
+
|
|
307
|
+
"#{((part.to_f / whole) * 100).round(1)}%"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def totals_lines
|
|
311
|
+
covered = covered_count
|
|
312
|
+
uncovered = uncovered_count
|
|
313
|
+
[
|
|
314
|
+
"#{color(:bold)}totals#{color(:reset)}",
|
|
315
|
+
"",
|
|
316
|
+
" total lines of code: #{@total_source_lines}",
|
|
317
|
+
" eligible lines: #{@total_eligible} " \
|
|
318
|
+
"#{color(:gray)}(#{percent(@total_eligible, @total_source_lines)} of total)#{color(:reset)}",
|
|
319
|
+
" #{color(:green)}covered (tests catch removal):#{color(:reset)} " \
|
|
320
|
+
"#{covered} #{color(:gray)}(#{percent(covered, @total_eligible)} of eligible)#{color(:reset)}",
|
|
321
|
+
" #{color(:red)}not covered (safely removable):#{color(:reset)} " \
|
|
322
|
+
"#{uncovered} #{color(:gray)}(#{percent(uncovered, @total_eligible)} of eligible)#{color(:reset)}"
|
|
323
|
+
]
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def draw_totals
|
|
327
|
+
@io.puts
|
|
328
|
+
@io.puts draw_box(totals_lines, :cyan)
|
|
329
|
+
@io.puts " #{color(:gray)}uncovered lines written to #{@output_path}#{color(:reset)}" if @output_path
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def print_totals_plain
|
|
333
|
+
covered = covered_count
|
|
334
|
+
uncovered = uncovered_count
|
|
335
|
+
@io.puts
|
|
336
|
+
@io.puts "totals:"
|
|
337
|
+
@io.puts " total lines of code: #{@total_source_lines}"
|
|
338
|
+
@io.puts " eligible lines: #{@total_eligible} " \
|
|
339
|
+
"(#{percent(@total_eligible, @total_source_lines)} of total)"
|
|
340
|
+
@io.puts " covered (tests catch removal): #{covered} " \
|
|
341
|
+
"(#{percent(covered, @total_eligible)} of eligible)"
|
|
342
|
+
@io.puts " not covered (safely removable): #{uncovered} " \
|
|
343
|
+
"(#{percent(uncovered, @total_eligible)} of eligible)"
|
|
344
|
+
@io.puts " uncovered lines written to #{@output_path}" if @output_path
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def write_uncovered_file
|
|
348
|
+
return unless @output_path
|
|
349
|
+
|
|
350
|
+
File.open(@output_path, "w") do |f|
|
|
351
|
+
f.puts "# mutante uncovered lines"
|
|
352
|
+
f.puts "# total lines of code: #{@total_source_lines}"
|
|
353
|
+
f.puts "# eligible lines: #{@total_eligible} " \
|
|
354
|
+
"(#{percent(@total_eligible, @total_source_lines)} of total)"
|
|
355
|
+
f.puts "# covered (tests catch removal): #{covered_count} " \
|
|
356
|
+
"(#{percent(covered_count, @total_eligible)} of eligible)"
|
|
357
|
+
f.puts "# not covered (safely removable): #{uncovered_count} " \
|
|
358
|
+
"(#{percent(uncovered_count, @total_eligible)} of eligible)"
|
|
359
|
+
f.puts
|
|
360
|
+
@flagged.each do |entry|
|
|
361
|
+
f.puts "#{entry[:path]}:#{entry[:line]}: #{entry[:text].strip}"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
247
366
|
def format_duration(seconds)
|
|
248
367
|
return "0s" if seconds.nil? || seconds.negative?
|
|
249
368
|
|
data/lib/mutante/runner.rb
CHANGED
|
@@ -3,11 +3,12 @@ module Mutante
|
|
|
3
3
|
def initialize(configuration: Mutante.configuration,
|
|
4
4
|
reporter: nil,
|
|
5
5
|
verbose: false,
|
|
6
|
+
output_path: "mutante_uncovered.txt",
|
|
6
7
|
test_runner: nil,
|
|
7
8
|
file_finder: nil,
|
|
8
9
|
spec_finder: nil)
|
|
9
10
|
@configuration = configuration
|
|
10
|
-
@reporter = reporter || Reporter.new(force_plain: verbose)
|
|
11
|
+
@reporter = reporter || Reporter.new(force_plain: verbose, output_path: output_path)
|
|
11
12
|
@verbose = verbose
|
|
12
13
|
@finder = file_finder || FileFinder.new(configuration)
|
|
13
14
|
@spec_finder = spec_finder || SpecFinder.new(configuration)
|
|
@@ -49,7 +50,7 @@ module Mutante
|
|
|
49
50
|
end
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
@reporter.file_finished
|
|
53
|
+
@reporter.file_finished(total_lines: analyzer.total_lines)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def relative_path(path)
|
data/lib/mutante/version.rb
CHANGED