mutant 0.14.1 → 0.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d3a480fb26d943cb4a6b83edab4a59c8f82e887af44f2915d713187f3a1de52
4
- data.tar.gz: fa67cd60285d710dea30b5de41d241f565b4fa1e199d957c05b2e250f3b4a71d
3
+ metadata.gz: 19ddb5cce03b640bb465057b2e6a0ee983b44b3990fff144dcd3a737b6e7ba5c
4
+ data.tar.gz: c97e3a1ac3fc5f4e431e70e86ee477933f1fa59e9ce338308a785c7fdab990c5
5
5
  SHA512:
6
- metadata.gz: 73587288bb7e4d7c940e5fb2ed4d06c3d18c2c0e13a29e453014d2034bb84018c17b43bb6419faa51aed03363458343a83f73a016a13da21be5faea77b168b9b
7
- data.tar.gz: fb6bd3cc6bd882b2fba0bf576de3076371d9f83711c13ea8bc88d520e36ebae516f3d618316c89092c78e4bb857018404512cf1c88d0cd52631145ca8a754c60
6
+ metadata.gz: aca0d3b5772f1b1caf6d075779dd6b54e02a4109917bc85f69b84afb038060f4610306b5f95c47f19cf429d287ffb4913b4cd5d71c19777a3f00a3df7cc7904e
7
+ data.tar.gz: 3dd977fa2018eac8935f471266af5fa77f1ca02095c130a3852f342fca3e2e099ab2cfabc75b746e3a44bcbbeeef9adf9979f92469e87df46227f98139fc859c
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.1
1
+ 0.14.2
@@ -10,13 +10,18 @@ module Mutant
10
10
  NAME = :full
11
11
 
12
12
  SELECTOR_REPLACEMENTS = {
13
+ :& => %i[| ^],
13
14
  :< => %i[== eql? equal?],
15
+ :<< => %i[>>],
14
16
  :<= => %i[< == eql? equal?],
15
17
  :== => %i[eql? equal?],
16
18
  :=== => %i[is_a?],
17
19
  :=~ => %i[match?],
18
20
  :> => %i[== eql? equal?],
19
21
  :>= => %i[> == eql? equal?],
22
+ :>> => %i[<<],
23
+ :^ => %i[& |],
24
+ :| => %i[& ^],
20
25
  __send__: %i[public_send],
21
26
  all?: %i[any?],
22
27
  any?: %i[all?],
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'io/console'
4
+
3
5
  module Mutant
4
6
  class Reporter
5
7
  class CLI
@@ -7,7 +9,20 @@ module Mutant
7
9
  #
8
10
  # rubocop:disable Style/FormatString
9
11
  class Format
10
- include AbstractType, Anima.new(:tty)
12
+ include AbstractType, Anima.new(:tty, :output_io)
13
+
14
+ DEFAULT_TERMINAL_WIDTH = 80
15
+
16
+ # Dynamic terminal width - queries current size on each call
17
+ #
18
+ # @return [Integer]
19
+ def terminal_width
20
+ return DEFAULT_TERMINAL_WIDTH unless tty && output_io.respond_to?(:winsize)
21
+
22
+ output_io.winsize.last
23
+ rescue Errno::ENOTTY, Errno::EOPNOTSUPP
24
+ DEFAULT_TERMINAL_WIDTH
25
+ end
11
26
 
12
27
  # Start representation
13
28
  #
@@ -35,7 +50,7 @@ module Mutant
35
50
 
36
51
  # Output abstraction to decouple tty? from buffer
37
52
  class Output
38
- include Anima.new(:tty, :buffer)
53
+ include Anima.new(:tty, :buffer, :terminal_width)
39
54
 
40
55
  # Test if output is a tty
41
56
  #
@@ -54,7 +69,7 @@ module Mutant
54
69
 
55
70
  def format(printer, object)
56
71
  buffer = new_buffer
57
- printer.call(output: Output.new(tty:, buffer:), object:)
72
+ printer.call(output: Output.new(tty:, buffer:, terminal_width:), object:)
58
73
  buffer.rewind
59
74
  buffer.read
60
75
  end
@@ -65,6 +80,14 @@ module Mutant
65
80
  REPORT_FREQUENCY = 1.0
66
81
  REPORT_DELAY = 1 / REPORT_FREQUENCY
67
82
 
83
+ # ANSI escape sequences
84
+ CLEAR_LINE = "\e[2K"
85
+ CURSOR_UP = "\e[A"
86
+ CURSOR_DOWN = "\e[B"
87
+
88
+ # Pattern to strip ANSI escape codes for visual length calculation
89
+ ANSI_ESCAPE = /\e\[[0-9;]*[A-Za-z]/
90
+
68
91
  # Start representation
69
92
  #
70
93
  # @return [String]
@@ -83,14 +106,14 @@ module Mutant
83
106
  #
84
107
  # @return [String]
85
108
  def progress(status)
86
- format(Printer::StatusProgressive, status)
109
+ wrap_progress { format(status_progressive_printer, status) }
87
110
  end
88
111
 
89
112
  # Progress representation
90
113
  #
91
114
  # @return [String]
92
115
  def test_progress(status)
93
- format(Printer::Test::StatusProgressive, status)
116
+ wrap_progress { format(test_status_progressive_printer, status) }
94
117
  end
95
118
 
96
119
  private
@@ -99,6 +122,75 @@ module Mutant
99
122
  StringIO.new
100
123
  end
101
124
 
125
+ def status_progressive_printer
126
+ tty ? Printer::StatusProgressive::Tty : Printer::StatusProgressive::Pipe
127
+ end
128
+
129
+ def test_status_progressive_printer
130
+ tty ? Printer::Test::StatusProgressive::Tty : Printer::Test::StatusProgressive::Pipe
131
+ end
132
+
133
+ # Wrap progress output with TTY-specific line handling
134
+ #
135
+ # Uses indicatif-style multi-line clearing to handle terminal resize:
136
+ # 1. Calculate how many visual lines previous content spans at current width
137
+ # 2. Clear all those lines using cursor movement
138
+ # 3. Write new content
139
+ #
140
+ # @return [String]
141
+ def wrap_progress
142
+ content = yield
143
+ return content unless tty
144
+
145
+ clear_seq = clear_last_lines(visual_lines_at_width(@last_content_length, terminal_width))
146
+ @last_content_length = visual_length(content)
147
+
148
+ "#{clear_seq}#{content}"
149
+ end
150
+
151
+ # Calculate visual length of string (excluding ANSI escape sequences)
152
+ #
153
+ # @param string [String]
154
+ # @return [Integer]
155
+ def visual_length(string)
156
+ string.gsub(ANSI_ESCAPE, '').length
157
+ end
158
+
159
+ # Calculate visual lines content occupies at given terminal width
160
+ #
161
+ # @param content_length [Integer, nil]
162
+ # @param width [Integer]
163
+ # @return [Integer]
164
+ def visual_lines_at_width(content_length, width)
165
+ return 1 if width < 1
166
+
167
+ # nil.to_f = 0.0, and ceil(0.0/w).clamp(1,10) = 1
168
+ (content_length.to_f / width).ceil.clamp(1, 10)
169
+ end
170
+
171
+ # Build escape sequence to clear n lines (indicatif/console pattern)
172
+ #
173
+ # Algorithm from console crate's clear_last_lines:
174
+ # 1. Move cursor up (n-1) lines to reach top
175
+ # 2. For each line: clear it, move down (except last)
176
+ # 3. Move cursor back up (n-1) lines
177
+ #
178
+ # For n=1: produces "\r\e[2K" (no cursor movement needed)
179
+ # For n>1: moves up, clears each line with downs between, moves back up
180
+ #
181
+ # @param lines [Integer] number of lines to clear (must be >= 1)
182
+ # @return [String]
183
+ def clear_last_lines(lines)
184
+ buffer = StringIO.new
185
+ buffer << (CURSOR_UP * (lines - 1))
186
+ lines.times do |i|
187
+ buffer << "\r" << CLEAR_LINE
188
+ buffer << CURSOR_DOWN if i < lines - 1
189
+ end
190
+ buffer << (CURSOR_UP * (lines - 1))
191
+ buffer.string
192
+ end
193
+
102
194
  end # Progressive
103
195
  end # Format
104
196
  end # CLI
@@ -46,7 +46,9 @@ module Mutant
46
46
  end
47
47
 
48
48
  def coverage_percent
49
- coverage * 100
49
+ # Floor to 2 decimal places to prevent rounding up to 100%
50
+ # when there are surviving mutations
51
+ (coverage * 10_000).floor / 100.0
50
52
  end
51
53
 
52
54
  def efficiency_percent
@@ -4,10 +4,8 @@ module Mutant
4
4
  class Reporter
5
5
  class CLI
6
6
  class Printer
7
- # Reporter for progressive output format on scheduler Status objects
7
+ # Base class for progressive status output
8
8
  class StatusProgressive < self
9
- FORMAT = 'progress: %02d/%02d alive: %d runtime: %0.02fs killtime: %0.02fs mutations/s: %0.02f'
10
-
11
9
  delegate(
12
10
  :amount_mutation_results,
13
11
  :amount_mutations,
@@ -17,21 +15,6 @@ module Mutant
17
15
  :runtime
18
16
  )
19
17
 
20
- # Run printer
21
- #
22
- # @return [undefined]
23
- def run
24
- status(
25
- FORMAT,
26
- amount_mutation_results,
27
- amount_mutations,
28
- amount_mutations_alive,
29
- runtime,
30
- killtime,
31
- mutations_per_second
32
- )
33
- end
34
-
35
18
  private
36
19
 
37
20
  def object
@@ -41,6 +24,50 @@ module Mutant
41
24
  def mutations_per_second
42
25
  amount_mutation_results / runtime
43
26
  end
27
+
28
+ # Pipe output format (non-TTY)
29
+ class Pipe < StatusProgressive
30
+ FORMAT = 'progress: %02d/%02d alive: %d runtime: %0.02fs killtime: %0.02fs mutations/s: %0.02f'
31
+
32
+ def run
33
+ status(
34
+ FORMAT,
35
+ amount_mutation_results,
36
+ amount_mutations,
37
+ amount_mutations_alive,
38
+ runtime,
39
+ killtime,
40
+ mutations_per_second
41
+ )
42
+ end
43
+ end # Pipe
44
+
45
+ # TTY output format with progress bar
46
+ class Tty < StatusProgressive
47
+ FORMAT = '%s %d/%d (%5.1f%%) %s alive: %d %0.1fs %0.2f/s'
48
+ MAX_BAR_WIDTH = 40
49
+ PREFIX = 'RUNNING'
50
+ PERCENTAGE_ESTIMATE = 99.9
51
+
52
+ def run
53
+ bar = ProgressBar.build(current: amount_mutation_results, total: amount_mutations, width: bar_width)
54
+ line = FORMAT % format_args(bar.percentage, bar.render)
55
+ output.write(colorize(status_color, line))
56
+ end
57
+
58
+ private
59
+
60
+ def format_args(percentage, bar)
61
+ [PREFIX, amount_mutation_results, amount_mutations, percentage, bar,
62
+ amount_mutations_alive, runtime, mutations_per_second]
63
+ end
64
+
65
+ def bar_width
66
+ non_bar_content = FORMAT % format_args(PERCENTAGE_ESTIMATE, nil)
67
+ available_width = output.terminal_width - non_bar_content.length
68
+ available_width.clamp(0, MAX_BAR_WIDTH)
69
+ end
70
+ end # Tty
44
71
  end # StatusProgressive
45
72
  end # Printer
46
73
  end # CLI
@@ -115,10 +115,8 @@ module Mutant
115
115
 
116
116
  end # Result
117
117
 
118
- # Reporter for progressive output format on scheduler Status objects
118
+ # Base class for progressive test status output
119
119
  class StatusProgressive < self
120
- FORMAT = 'progress: %02d/%02d failed: %d runtime: %0.02fs testtime: %0.02fs tests/s: %0.02f'
121
-
122
120
  delegate(
123
121
  :amount_test_results,
124
122
  :amount_tests,
@@ -127,21 +125,6 @@ module Mutant
127
125
  :runtime
128
126
  )
129
127
 
130
- # Run printer
131
- #
132
- # @return [undefined]
133
- def run
134
- status(
135
- FORMAT,
136
- amount_test_results,
137
- amount_tests,
138
- amount_tests_failed,
139
- runtime,
140
- testtime,
141
- tests_per_second
142
- )
143
- end
144
-
145
128
  private
146
129
 
147
130
  def object
@@ -151,6 +134,50 @@ module Mutant
151
134
  def tests_per_second
152
135
  amount_test_results / runtime
153
136
  end
137
+
138
+ # Pipe output format (non-TTY)
139
+ class Pipe < StatusProgressive
140
+ FORMAT = 'progress: %02d/%02d failed: %d runtime: %0.02fs testtime: %0.02fs tests/s: %0.02f'
141
+
142
+ def run
143
+ status(
144
+ FORMAT,
145
+ amount_test_results,
146
+ amount_tests,
147
+ amount_tests_failed,
148
+ runtime,
149
+ testtime,
150
+ tests_per_second
151
+ )
152
+ end
153
+ end # Pipe
154
+
155
+ # TTY output format with progress bar
156
+ class Tty < StatusProgressive
157
+ FORMAT = '%s %d/%d (%5.1f%%) %s failed: %d %0.1fs %0.2f/s'
158
+ MAX_BAR_WIDTH = 40
159
+ PREFIX = 'TESTING'
160
+ PERCENTAGE_ESTIMATE = 99.9
161
+
162
+ def run
163
+ bar = ProgressBar.build(current: amount_test_results, total: amount_tests, width: bar_width)
164
+ line = FORMAT % format_args(bar.percentage, bar.render)
165
+ output.write(colorize(status_color, line))
166
+ end
167
+
168
+ private
169
+
170
+ def format_args(percentage, bar)
171
+ [PREFIX, amount_test_results, amount_tests, percentage, bar,
172
+ amount_tests_failed, runtime, tests_per_second]
173
+ end
174
+
175
+ def bar_width
176
+ non_bar_content = FORMAT % format_args(PERCENTAGE_ESTIMATE, nil)
177
+ available_width = output.terminal_width - non_bar_content.length
178
+ available_width.clamp(0, MAX_BAR_WIDTH)
179
+ end
180
+ end # Tty
154
181
  end # StatusProgressive
155
182
  end # Test
156
183
  end # Printer
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Reporter
5
+ class CLI
6
+ # Visual progress bar renderer
7
+ #
8
+ # Renders nextest-style progress bars like:
9
+ # 45/100 (45.0%) ████████████░░░░░░░░ alive: 12 23.45s
10
+ class ProgressBar
11
+ include Anima.new(
12
+ :current,
13
+ :total,
14
+ :width,
15
+ :filled_char,
16
+ :empty_char
17
+ )
18
+
19
+ FILLED_CHAR = "\u2588" # █
20
+ EMPTY_CHAR = "\u2591" # ░
21
+
22
+ DEFAULT_WIDTH = 30
23
+
24
+ # Render the progress bar string
25
+ #
26
+ # @return [String]
27
+ def render
28
+ "#{filled}#{empty}"
29
+ end
30
+
31
+ # Calculate percentage completion
32
+ #
33
+ # @return [Float]
34
+ def percentage
35
+ return 0.0 if total.zero?
36
+
37
+ (current.to_f / total * 100)
38
+ end
39
+
40
+ # Build a progress bar with defaults
41
+ #
42
+ # @param current [Integer] current progress value
43
+ # @param total [Integer] total value
44
+ # @param width [Integer] bar width in characters
45
+ #
46
+ # @return [ProgressBar]
47
+ def self.build(current:, total:, width: DEFAULT_WIDTH)
48
+ new(
49
+ current:,
50
+ total:,
51
+ width:,
52
+ filled_char: FILLED_CHAR,
53
+ empty_char: EMPTY_CHAR
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ def filled_width
60
+ return 0 if total.zero?
61
+
62
+ [((current.to_f / total) * width).round, width].min
63
+ end
64
+
65
+ def empty_width
66
+ width - filled_width
67
+ end
68
+
69
+ def filled
70
+ filled_char * filled_width
71
+ end
72
+
73
+ def empty
74
+ empty_char * empty_width
75
+ end
76
+ end # ProgressBar
77
+ end # CLI
78
+ end # Reporter
79
+ end # Mutant
@@ -12,8 +12,10 @@ module Mutant
12
12
  #
13
13
  # @return [Reporter::CLI]
14
14
  def self.build(output)
15
+ tty = output.respond_to?(:tty?) && output.tty?
16
+
15
17
  new(
16
- format: Format::Progressive.new(tty: output.respond_to?(:tty?) && output.tty?),
18
+ format: Format::Progressive.new(tty:, output_io: output),
17
19
  print_warnings: false,
18
20
  output:
19
21
  )
@@ -80,6 +82,7 @@ module Mutant
80
82
  #
81
83
  # @return [self]
82
84
  def report(env)
85
+ write_final_progress(env)
83
86
  Printer::EnvResult.call(output:, object: env)
84
87
  self
85
88
  end
@@ -90,6 +93,7 @@ module Mutant
90
93
  #
91
94
  # @return [self]
92
95
  def test_report(env)
96
+ write_final_test_progress(env)
93
97
  Printer::Test::EnvResult.call(output:, object: env)
94
98
  self
95
99
  end
@@ -100,6 +104,30 @@ module Mutant
100
104
  output.write(frame)
101
105
  end
102
106
 
107
+ def write_final_progress(env)
108
+ return unless format.tty
109
+
110
+ final_status = Parallel::Status.new(
111
+ active_jobs: Set.new,
112
+ done: true,
113
+ payload: env
114
+ )
115
+ write(format.progress(final_status))
116
+ output.puts
117
+ end
118
+
119
+ def write_final_test_progress(env)
120
+ return unless format.tty
121
+
122
+ final_status = Parallel::Status.new(
123
+ active_jobs: Set.new,
124
+ done: true,
125
+ payload: env
126
+ )
127
+ write(format.test_progress(final_status))
128
+ output.puts
129
+ end
130
+
103
131
  end # CLI
104
132
  end # Reporter
105
133
  end # Mutant
data/lib/mutant.rb CHANGED
@@ -237,6 +237,7 @@ module Mutant
237
237
  require 'mutant/reporter/null'
238
238
  require 'mutant/reporter/sequence'
239
239
  require 'mutant/reporter/cli'
240
+ require 'mutant/reporter/cli/progress_bar'
240
241
  require 'mutant/reporter/cli/printer'
241
242
  require 'mutant/reporter/cli/printer/config'
242
243
  require 'mutant/reporter/cli/printer/coverage_result'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: 0.14.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Markus Schirp
@@ -327,6 +327,7 @@ files:
327
327
  - lib/mutant/reporter/cli/printer/status_progressive.rb
328
328
  - lib/mutant/reporter/cli/printer/subject_result.rb
329
329
  - lib/mutant/reporter/cli/printer/test.rb
330
+ - lib/mutant/reporter/cli/progress_bar.rb
330
331
  - lib/mutant/reporter/null.rb
331
332
  - lib/mutant/reporter/sequence.rb
332
333
  - lib/mutant/repository.rb
@@ -377,7 +378,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
377
378
  - !ruby/object:Gem::Version
378
379
  version: '0'
379
380
  requirements: []
380
- rubygems_version: 3.6.9
381
+ rubygems_version: 4.0.3
381
382
  specification_version: 4
382
383
  summary: ''
383
384
  test_files: []