mutant 0.13.5 → 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 +4 -4
- data/VERSION +1 -0
- data/bin/mutant +6 -59
- data/bin/mutant-ruby +64 -0
- data/lib/mutant/ast/node_predicates.rb +1 -1
- data/lib/mutant/bootstrap.rb +3 -5
- data/lib/mutant/matcher/methods.rb +9 -20
- data/lib/mutant/mutation/operators.rb +6 -1
- data/lib/mutant/mutator/node/named_value/access.rb +1 -25
- data/lib/mutant/parallel/connection.rb +12 -9
- data/lib/mutant/range.rb +1 -1
- data/lib/mutant/reporter/cli/format.rb +97 -5
- data/lib/mutant/reporter/cli/printer/env_progress.rb +3 -1
- data/lib/mutant/reporter/cli/printer/status_progressive.rb +45 -18
- data/lib/mutant/reporter/cli/printer/test.rb +45 -18
- data/lib/mutant/reporter/cli/progress_bar.rb +79 -0
- data/lib/mutant/reporter/cli.rb +29 -1
- data/lib/mutant/version.rb +17 -1
- data/lib/mutant.rb +1 -0
- metadata +12 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19ddb5cce03b640bb465057b2e6a0ee983b44b3990fff144dcd3a737b6e7ba5c
|
|
4
|
+
data.tar.gz: c97e3a1ac3fc5f4e431e70e86ee477933f1fa59e9ce338308a785c7fdab990c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aca0d3b5772f1b1caf6d075779dd6b54e02a4109917bc85f69b84afb038060f4610306b5f95c47f19cf429d287ffb4913b4cd5d71c19777a3f00a3df7cc7904e
|
|
7
|
+
data.tar.gz: 3dd977fa2018eac8935f471266af5fa77f1ca02095c130a3852f342fca3e2e099ab2cfabc75b746e3a44bcbbeeef9adf9979f92469e87df46227f98139fc859c
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.14.2
|
data/bin/mutant
CHANGED
|
@@ -1,64 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
# Dispatcher that selects between mutant-ruby and mutant-rust
|
|
5
|
+
# based on MUTANT_RUST environment variable.
|
|
6
|
+
#
|
|
7
|
+
# See RUST.md for documentation.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
effective_status = status ? status + 128 : 128
|
|
10
|
-
exit! effective_status
|
|
11
|
-
end
|
|
9
|
+
fail 'MUTANT_RUST=1 is not yet supported via gem installation' if ENV['MUTANT_RUST']
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
WORLD.record(:cli_parse) do
|
|
16
|
-
CLI.parse(
|
|
17
|
-
arguments: ARGV,
|
|
18
|
-
world: Mutant::WORLD
|
|
19
|
-
)
|
|
20
|
-
end.either(
|
|
21
|
-
->(message) { Mutant::WORLD.stderr.puts(message); Kernel.exit(false) },
|
|
22
|
-
# rubocop:disable Metrics/BlockLength
|
|
23
|
-
lambda do |command|
|
|
24
|
-
if command.zombie?
|
|
25
|
-
command = WORLD.record(:zombify) do
|
|
26
|
-
$stderr.puts('Running mutant zombified!')
|
|
27
|
-
Zombifier.call(
|
|
28
|
-
namespace: :Zombie,
|
|
29
|
-
load_path: $LOAD_PATH,
|
|
30
|
-
kernel: Kernel,
|
|
31
|
-
pathname: Pathname,
|
|
32
|
-
require_highjack: RequireHighjack
|
|
33
|
-
.public_method(:call)
|
|
34
|
-
.to_proc
|
|
35
|
-
.curry
|
|
36
|
-
.call(Kernel),
|
|
37
|
-
root_require: 'mutant',
|
|
38
|
-
includes: %w[
|
|
39
|
-
adamantium
|
|
40
|
-
anima
|
|
41
|
-
concord
|
|
42
|
-
equalizer
|
|
43
|
-
mprelude
|
|
44
|
-
mutant
|
|
45
|
-
unparser
|
|
46
|
-
variable
|
|
47
|
-
]
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
Zombie::Mutant::CLI.parse(
|
|
51
|
-
arguments: ARGV,
|
|
52
|
-
world: Mutant::WORLD
|
|
53
|
-
).from_right
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
WORLD.record(:execute) { command.call }.tap do |status|
|
|
58
|
-
WORLD.recorder.print_profile(WORLD.stderr) if command.print_profile?
|
|
59
|
-
WORLD.kernel.exit(status)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
# rubocop:enable Metrics/BlockLength
|
|
63
|
-
)
|
|
64
|
-
end
|
|
11
|
+
exec('mutant-ruby', *ARGV)
|
data/bin/mutant-ruby
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mutant
|
|
5
|
+
# Record executable timestamp
|
|
6
|
+
@executable_timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
7
|
+
|
|
8
|
+
trap('INT') do |status|
|
|
9
|
+
effective_status = status ? status + 128 : 128
|
|
10
|
+
exit! effective_status
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require 'mutant'
|
|
14
|
+
|
|
15
|
+
WORLD.record(:cli_parse) do
|
|
16
|
+
CLI.parse(
|
|
17
|
+
arguments: ARGV,
|
|
18
|
+
world: Mutant::WORLD
|
|
19
|
+
)
|
|
20
|
+
end.either(
|
|
21
|
+
->(message) { Mutant::WORLD.stderr.puts(message); Kernel.exit(false) },
|
|
22
|
+
# rubocop:disable Metrics/BlockLength
|
|
23
|
+
lambda do |command|
|
|
24
|
+
if command.zombie?
|
|
25
|
+
command = WORLD.record(:zombify) do
|
|
26
|
+
$stderr.puts('Running mutant zombified!')
|
|
27
|
+
Zombifier.call(
|
|
28
|
+
namespace: :Zombie,
|
|
29
|
+
load_path: $LOAD_PATH,
|
|
30
|
+
kernel: Kernel,
|
|
31
|
+
pathname: Pathname,
|
|
32
|
+
require_highjack: RequireHighjack
|
|
33
|
+
.public_method(:call)
|
|
34
|
+
.to_proc
|
|
35
|
+
.curry
|
|
36
|
+
.call(Kernel),
|
|
37
|
+
root_require: 'mutant',
|
|
38
|
+
includes: %w[
|
|
39
|
+
adamantium
|
|
40
|
+
anima
|
|
41
|
+
concord
|
|
42
|
+
equalizer
|
|
43
|
+
mprelude
|
|
44
|
+
mutant
|
|
45
|
+
unparser
|
|
46
|
+
variable
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
Zombie::Mutant::CLI.parse(
|
|
51
|
+
arguments: ARGV,
|
|
52
|
+
world: Mutant::WORLD
|
|
53
|
+
).from_right
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
WORLD.record(:execute) { command.call }.tap do |status|
|
|
58
|
+
WORLD.recorder.print_profile(WORLD.stderr) if command.print_profile?
|
|
59
|
+
WORLD.kernel.exit(status)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
# rubocop:enable Metrics/BlockLength
|
|
63
|
+
)
|
|
64
|
+
end
|
|
@@ -6,7 +6,7 @@ module Mutant
|
|
|
6
6
|
module NodePredicates
|
|
7
7
|
|
|
8
8
|
Types::ALL.each do |type|
|
|
9
|
-
fail "method: #{type} is already defined" if
|
|
9
|
+
fail "method: #{type} is already defined" if method_defined?(type)
|
|
10
10
|
|
|
11
11
|
name = "n_#{type.to_s.chomp('?')}?"
|
|
12
12
|
|
data/lib/mutant/bootstrap.rb
CHANGED
|
@@ -73,7 +73,6 @@ module Mutant
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# rubocop:disable Metrics/MethodLength
|
|
76
|
-
# rubocop:disable Style/MultilineBlockChain
|
|
77
76
|
def self.setup_integration(env:, mutations:, selected_subjects:)
|
|
78
77
|
env.record(__method__) do
|
|
79
78
|
hooks = env.hooks
|
|
@@ -84,14 +83,13 @@ module Mutant
|
|
|
84
83
|
mutations:,
|
|
85
84
|
selector: Selector::Expression.new(integration:),
|
|
86
85
|
subjects: selected_subjects
|
|
87
|
-
)
|
|
88
|
-
end
|
|
86
|
+
).tap { hooks.run(:setup_integration_post) }
|
|
87
|
+
end
|
|
89
88
|
end
|
|
90
89
|
end
|
|
91
90
|
private_class_method :setup_integration
|
|
92
|
-
# rubocop:enable Metrics/MethodLength
|
|
93
|
-
# rubocop:enable Style/MultilineBlockChain
|
|
94
91
|
|
|
92
|
+
# rubocop:enable Metrics/MethodLength
|
|
95
93
|
def self.load_hooks(env)
|
|
96
94
|
env.record(__method__) do
|
|
97
95
|
env.with(hooks: Hooks.load_config(env.config))
|
|
@@ -36,7 +36,7 @@ module Mutant
|
|
|
36
36
|
def methods(env)
|
|
37
37
|
candidate_names.each_with_object([]) do |name, methods|
|
|
38
38
|
method = access(env, name)
|
|
39
|
-
methods << method if method
|
|
39
|
+
methods << method if method
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -50,10 +50,8 @@ module Mutant
|
|
|
50
50
|
abstract_method :candidate_scope
|
|
51
51
|
private :candidate_scope
|
|
52
52
|
|
|
53
|
-
# Matcher for
|
|
54
|
-
class
|
|
55
|
-
MATCHER = Matcher::Method::Singleton
|
|
56
|
-
|
|
53
|
+
# Matcher for eigenclass methods
|
|
54
|
+
class Eigenclass < self
|
|
57
55
|
private
|
|
58
56
|
|
|
59
57
|
def access(_env, method_name)
|
|
@@ -63,22 +61,16 @@ module Mutant
|
|
|
63
61
|
def candidate_scope
|
|
64
62
|
scope.raw.singleton_class
|
|
65
63
|
end
|
|
64
|
+
end # Eigenclass
|
|
66
65
|
|
|
66
|
+
# Matcher for singleton methods
|
|
67
|
+
class Singleton < Eigenclass
|
|
68
|
+
MATCHER = Matcher::Method::Singleton
|
|
67
69
|
end # Singleton
|
|
68
70
|
|
|
69
71
|
# Matcher for metaclass methods
|
|
70
|
-
class Metaclass <
|
|
72
|
+
class Metaclass < Eigenclass
|
|
71
73
|
MATCHER = Matcher::Method::Metaclass
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
def access(_env, method_name)
|
|
76
|
-
scope.raw.method(method_name)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def candidate_scope
|
|
80
|
-
scope.raw.singleton_class
|
|
81
|
-
end
|
|
82
74
|
end # Metaclass
|
|
83
75
|
|
|
84
76
|
# Matcher for instance methods
|
|
@@ -104,17 +96,14 @@ module Mutant
|
|
|
104
96
|
|
|
105
97
|
private
|
|
106
98
|
|
|
107
|
-
# rubocop:disable Lint/RescueException
|
|
108
|
-
# mutant:disable - unstable source locations under < ruby-3.2
|
|
109
99
|
def access(env, method_name)
|
|
110
100
|
candidate_scope.instance_method(method_name)
|
|
111
|
-
rescue
|
|
101
|
+
rescue => exception
|
|
112
102
|
env.warn(
|
|
113
103
|
MESSAGE % { scope:, method_name:, exception: exception.inspect }
|
|
114
104
|
)
|
|
115
105
|
nil
|
|
116
106
|
end
|
|
117
|
-
# rubocop:enable Lint/RescueException
|
|
118
107
|
|
|
119
108
|
def candidate_scope
|
|
120
109
|
scope.raw
|
|
@@ -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?],
|
|
@@ -46,7 +51,7 @@ module Mutant
|
|
|
46
51
|
to_i: %i[to_int],
|
|
47
52
|
to_s: %i[to_str],
|
|
48
53
|
values_at: %i[fetch_values]
|
|
49
|
-
}.freeze.tap { |hash| hash.
|
|
54
|
+
}.freeze.tap { |hash| hash.each_value(&:freeze) }
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
class Light < self
|
|
@@ -8,7 +8,7 @@ module Mutant
|
|
|
8
8
|
# Mutation emitter to handle named value access nodes
|
|
9
9
|
class Access < Node
|
|
10
10
|
|
|
11
|
-
handle(:gvar, :cvar, :lvar, :self)
|
|
11
|
+
handle(:gvar, :cvar, :ivar, :lvar, :self)
|
|
12
12
|
|
|
13
13
|
private
|
|
14
14
|
|
|
@@ -16,30 +16,6 @@ module Mutant
|
|
|
16
16
|
emit_singletons
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Named value access emitter for instance variables
|
|
20
|
-
class Ivar < Access
|
|
21
|
-
NAME_RANGE = (1..-1)
|
|
22
|
-
|
|
23
|
-
handle(:ivar)
|
|
24
|
-
|
|
25
|
-
children :name
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def dispatch
|
|
30
|
-
emit_attribute_read
|
|
31
|
-
super
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def emit_attribute_read
|
|
35
|
-
emit(s(:send, nil, attribute_name))
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def attribute_name
|
|
39
|
-
name.slice(NAME_RANGE).to_sym
|
|
40
|
-
end
|
|
41
|
-
end # Ivar
|
|
42
|
-
|
|
43
19
|
end # Access
|
|
44
20
|
end # NamedValue
|
|
45
21
|
end # Node
|
|
@@ -10,6 +10,7 @@ module Mutant
|
|
|
10
10
|
HEADER_FORMAT = 'N'
|
|
11
11
|
HEADER_SIZE = 4
|
|
12
12
|
MAX_BYTES = (2**32).pred
|
|
13
|
+
MAX_LOG_CHUNK = 4096
|
|
13
14
|
|
|
14
15
|
class Reader
|
|
15
16
|
include Anima.new(:deadline, :io, :marshal, :response_reader, :log_reader)
|
|
@@ -86,10 +87,10 @@ module Mutant
|
|
|
86
87
|
|
|
87
88
|
def advance_result
|
|
88
89
|
if length
|
|
89
|
-
if
|
|
90
|
+
if read_result_buffer(length)
|
|
90
91
|
@results << marshal.load(@buffer)
|
|
91
92
|
end
|
|
92
|
-
elsif
|
|
93
|
+
elsif read_result_buffer(HEADER_SIZE)
|
|
93
94
|
@lengths << Util.one(@buffer.unpack(HEADER_FORMAT))
|
|
94
95
|
@buffer = +''
|
|
95
96
|
end
|
|
@@ -100,17 +101,16 @@ module Mutant
|
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
def advance_log
|
|
103
|
-
with_nonblock_read(io: log_reader, max_bytes:
|
|
104
|
+
while with_nonblock_read(io: log_reader, max_bytes: MAX_LOG_CHUNK, &@log.public_method(:<<))
|
|
105
|
+
end
|
|
104
106
|
end
|
|
105
107
|
|
|
106
|
-
def
|
|
108
|
+
def read_result_buffer(max_bytes)
|
|
107
109
|
with_nonblock_read(
|
|
108
110
|
io: response_reader,
|
|
109
|
-
max_bytes: max_bytes - @buffer.bytesize
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@buffer.bytesize.equal?(max_bytes)
|
|
113
|
-
end
|
|
111
|
+
max_bytes: max_bytes - @buffer.bytesize,
|
|
112
|
+
&@buffer.public_method(:<<)
|
|
113
|
+
)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
# rubocop:disable Metrics/MethodLength
|
|
@@ -123,8 +123,11 @@ module Mutant
|
|
|
123
123
|
when nil
|
|
124
124
|
@errors << EOFError
|
|
125
125
|
false
|
|
126
|
+
when :wait_readable
|
|
127
|
+
false
|
|
126
128
|
when String
|
|
127
129
|
yield chunk
|
|
130
|
+
chunk.bytesize.equal?(max_bytes)
|
|
128
131
|
else
|
|
129
132
|
fail "Unexpected nonblocking read return: #{chunk.inspect}"
|
|
130
133
|
end
|
data/lib/mutant/range.rb
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
@@ -4,10 +4,8 @@ module Mutant
|
|
|
4
4
|
class Reporter
|
|
5
5
|
class CLI
|
|
6
6
|
class Printer
|
|
7
|
-
#
|
|
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
|
-
#
|
|
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
|
data/lib/mutant/reporter/cli.rb
CHANGED
|
@@ -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
|
|
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/version.rb
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
3
5
|
module Mutant
|
|
4
6
|
# Current mutant version
|
|
5
|
-
|
|
7
|
+
#
|
|
8
|
+
# See RUST.md for documentation on version loading behavior.
|
|
9
|
+
VERSION =
|
|
10
|
+
if ENV['MUTANT_RUST']
|
|
11
|
+
ENV.fetch('MUTANT_VERSION').freeze
|
|
12
|
+
else
|
|
13
|
+
Pathname
|
|
14
|
+
.new(__dir__)
|
|
15
|
+
.parent
|
|
16
|
+
.parent
|
|
17
|
+
.join('VERSION')
|
|
18
|
+
.read
|
|
19
|
+
.chomp
|
|
20
|
+
.freeze
|
|
21
|
+
end
|
|
6
22
|
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.
|
|
4
|
+
version: 0.14.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Markus Schirp
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 1.15
|
|
32
|
+
version: '1.15'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 1.15
|
|
39
|
+
version: '1.15'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: parser
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -71,14 +71,14 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 0.
|
|
74
|
+
version: 0.6.0
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: 0.
|
|
81
|
+
version: 0.6.0
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: unparser
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -127,14 +127,14 @@ dependencies:
|
|
|
127
127
|
requirements:
|
|
128
128
|
- - "~>"
|
|
129
129
|
- !ruby/object:Gem::Version
|
|
130
|
-
version:
|
|
130
|
+
version: '2.0'
|
|
131
131
|
type: :development
|
|
132
132
|
prerelease: false
|
|
133
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
134
|
requirements:
|
|
135
135
|
- - "~>"
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
|
-
version:
|
|
137
|
+
version: '2.0'
|
|
138
138
|
- !ruby/object:Gem::Dependency
|
|
139
139
|
name: rubocop
|
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -154,12 +154,15 @@ email:
|
|
|
154
154
|
- mbj@schirp-dso.com
|
|
155
155
|
executables:
|
|
156
156
|
- mutant
|
|
157
|
+
- mutant-ruby
|
|
157
158
|
extensions: []
|
|
158
159
|
extra_rdoc_files:
|
|
159
160
|
- LICENSE
|
|
160
161
|
files:
|
|
161
162
|
- LICENSE
|
|
163
|
+
- VERSION
|
|
162
164
|
- bin/mutant
|
|
165
|
+
- bin/mutant-ruby
|
|
163
166
|
- lib/mutant.rb
|
|
164
167
|
- lib/mutant/ast.rb
|
|
165
168
|
- lib/mutant/ast/find_metaclass_containing.rb
|
|
@@ -324,6 +327,7 @@ files:
|
|
|
324
327
|
- lib/mutant/reporter/cli/printer/status_progressive.rb
|
|
325
328
|
- lib/mutant/reporter/cli/printer/subject_result.rb
|
|
326
329
|
- lib/mutant/reporter/cli/printer/test.rb
|
|
330
|
+
- lib/mutant/reporter/cli/progress_bar.rb
|
|
327
331
|
- lib/mutant/reporter/null.rb
|
|
328
332
|
- lib/mutant/reporter/sequence.rb
|
|
329
333
|
- lib/mutant/repository.rb
|
|
@@ -374,7 +378,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
374
378
|
- !ruby/object:Gem::Version
|
|
375
379
|
version: '0'
|
|
376
380
|
requirements: []
|
|
377
|
-
rubygems_version:
|
|
381
|
+
rubygems_version: 4.0.3
|
|
378
382
|
specification_version: 4
|
|
379
383
|
summary: ''
|
|
380
384
|
test_files: []
|