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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5644bbc1cfc15fe2e6fe859d2ce4ee547f4780ea1215d62cb29408f48bbca591
4
- data.tar.gz: 451058c2475efd8c91a90575bd8100f35672eea29431fab6d8755429af4c58d5
3
+ metadata.gz: ae5a33de8e678194912023676158d0fa89fcd36654799e479c82bc6659c09850
4
+ data.tar.gz: dba11952956c4ebce0745a598d10202d86599348231d983b9025df7be5549de7
5
5
  SHA512:
6
- metadata.gz: dda544bdea5c32ddd89486e4cb459b20a24a6cb65a7793ea1378bbeda74e83f17761b3603618d42e00b1327ed12f001cc4cafc8506797bdd9f0b335ca18c43a5
7
- data.tar.gz: d961f33aaa7064aa9219fcc93d7bb98d6973a4192c19889d0232a25b84d2da9b6f7b82e2650ddd8f25805a4b9d7158c8c8535a73b150071636d3cf71e00381e3
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(verbose: @options[:verbose]).call(target)
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.
@@ -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
- @file_index += 1
73
- @current_file = path
74
- @counts = { tested: 0, flagged: 0, skipped: 0 }
75
- @file_started = monotonic_now
76
-
77
- if @tty
78
- @io.puts
79
- @io.puts "#{color(:cyan)}▸#{color(:reset)} " \
80
- "#{color(:bold)}#{path}#{color(:reset)} " \
81
- "#{color(:gray)}[#{@file_index}/#{@total_files}]#{color(:reset)}"
82
- redraw_status
83
- else
84
- @io.puts path
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
- @flagged << { path: path, line: line_number, text: line_text }
90
- @counts[:flagged] += 1 if @counts
91
- @counts[:tested] += 1 if @counts
92
-
93
- if @tty
94
- @io.print CLEAR_LINE
95
- @io.puts " #{color(:red)}⚑#{color(:reset)} " \
96
- "#{color(:red)}#{path}:#{line_number}#{color(:reset)} " \
97
- "#{color(:dim)}#{line_text.strip}#{color(:reset)}"
98
- redraw_status
99
- else
100
- @io.puts " flagged #{path}:#{line_number} #{line_text.strip}"
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
- @counts[:tested] += 1 if @counts
115
+ @mutex.synchronize do
116
+ @counts[:tested] += 1 if @counts
106
117
 
107
- if @tty
108
- redraw_status
109
- else
110
- @io.print "."
111
- @io.flush
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
- @counts[:skipped] += 1 if @counts
128
+ @mutex.synchronize do
129
+ @counts[:skipped] += 1 if @counts
117
130
 
118
- if @tty
119
- redraw_status
120
- else
121
- @io.print "-"
122
- @io.flush
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
- if @tty
128
- @io.print CLEAR_LINE
129
- elapsed = format_duration(monotonic_now - (@file_started || monotonic_now))
130
- icon = @counts && @counts[:flagged].positive? ? "#{color(:yellow)}⚑#{color(:reset)}" : "#{color(:green)}✓#{color(:reset)}"
131
- parts = []
132
- parts << "#{@counts[:tested]} tested" if @counts
133
- parts << "#{color(:red)}#{@counts[:flagged]} flagged#{color(:reset)}" if @counts && @counts[:flagged].positive?
134
- parts << "#{@counts[:skipped]} skipped" if @counts && @counts[:skipped].positive?
135
- parts << "#{color(:gray)}#{elapsed}#{color(:reset)}"
136
- @io.puts " #{icon} #{parts.join(color(:gray) + ' · ' + color(:reset))}"
137
- else
138
- @io.puts
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
 
@@ -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)
@@ -1,3 +1,3 @@
1
1
  module Mutante
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutante
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nazareno Moresco