mutant 0.15.1 → 0.16.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 -1
- data/lib/mutant/ast/pattern/lexer.rb +119 -47
- data/lib/mutant/cli/command/root.rb +1 -1
- data/lib/mutant/cli/command/session.rb +281 -0
- data/lib/mutant/cli/command.rb +16 -2
- data/lib/mutant/config.rb +1 -1
- data/lib/mutant/expression/method.rb +0 -2
- data/lib/mutant/expression/methods.rb +0 -2
- data/lib/mutant/expression/namespace.rb +0 -2
- data/lib/mutant/integration/null.rb +1 -1
- data/lib/mutant/isolation/fork.rb +3 -7
- data/lib/mutant/isolation/none.rb +1 -1
- data/lib/mutant/isolation.rb +31 -0
- data/lib/mutant/log_capture.rb +89 -0
- data/lib/mutant/matcher/null.rb +1 -1
- data/lib/mutant/mutation/runner/sink.rb +23 -10
- data/lib/mutant/mutation/runner.rb +1 -0
- data/lib/mutant/mutation.rb +3 -20
- data/lib/mutant/mutator/node/literal/integer.rb +61 -0
- data/lib/mutant/parallel/connection.rb +2 -4
- data/lib/mutant/parallel/driver.rb +0 -2
- data/lib/mutant/reporter/cli/printer/alive_results.rb +27 -0
- data/lib/mutant/reporter/cli/printer/env_result.rb +52 -9
- data/lib/mutant/reporter/cli/printer/isolation_result.rb +3 -6
- data/lib/mutant/reporter/cli/printer/subject_result.rb +103 -5
- data/lib/mutant/reporter/cli/printer/test.rb +1 -1
- data/lib/mutant/reporter/cli/printer.rb +24 -1
- data/lib/mutant/repository/diff.rb +1 -2
- data/lib/mutant/result/exception.rb +29 -0
- data/lib/mutant/result/json_writer.rb +43 -0
- data/lib/mutant/result/process_status.rb +37 -0
- data/lib/mutant/result/session.rb +63 -0
- data/lib/mutant/result/test.rb +57 -0
- data/lib/mutant/result.rb +201 -96
- data/lib/mutant/segment/recorder.rb +0 -2
- data/lib/mutant/test/runner/sink.rb +1 -1
- data/lib/mutant/timer.rb +3 -1
- data/lib/mutant/transform/codec.rb +45 -0
- data/lib/mutant/transform.rb +33 -25
- data/lib/mutant/world.rb +6 -0
- data/lib/mutant/zombifier.rb +0 -2
- data/lib/mutant.rb +14 -4
- metadata +34 -7
- data/lib/mutant/reporter/cli/printer/coverage_result.rb +0 -19
- data/lib/mutant/reporter/cli/printer/mutation_result.rb +0 -84
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f5176c82dd4c6a57785deebd566c3b29b14b4496d35c06e15c20ff3b98a0b37
|
|
4
|
+
data.tar.gz: 9f29a560a6c144480d8808cf24ec2ec2127b78bc7f9ef88e8e8a3e0f541e0974
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5e05c7b33041fb609ebcc89683461dba406c53592801d69603fa9003e0d8ee923f54d7b6f0bc9abcfd811ade64054ef3a8b9bf2bc30dbd44f37ca1f1ae2d4b6
|
|
7
|
+
data.tar.gz: 659e58f9ce856224a2a8f67d073c9006e340bcd838b0f6543cc57f8a12cbf6cc29c6dccad248744748a9f176750dae5240b719b797c7339a8552532e10d4ce22
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.16.2
|
|
@@ -3,20 +3,43 @@
|
|
|
3
3
|
module Mutant
|
|
4
4
|
class AST
|
|
5
5
|
class Pattern
|
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
|
6
7
|
class Lexer
|
|
7
|
-
WHITESPACE
|
|
8
|
-
STRING_PATTERN = /\A[a-zA-Z][_a-zA-Z0-9]*\z/
|
|
8
|
+
WHITESPACE = [' ', "\t", "\n"].to_set.freeze
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
STRUCTURAL =
|
|
11
11
|
{
|
|
12
12
|
'(' => :group_start,
|
|
13
13
|
')' => :group_end,
|
|
14
14
|
',' => :delimiter,
|
|
15
|
-
'=' => :eq,
|
|
16
15
|
'{' => :properties_start,
|
|
17
16
|
'}' => :properties_end
|
|
18
17
|
}.freeze
|
|
19
18
|
|
|
19
|
+
EQ_OPERATORS = %w[=== == =~].freeze
|
|
20
|
+
|
|
21
|
+
OPERATORS_BY_START =
|
|
22
|
+
{
|
|
23
|
+
'!' => %w[!= !~ !].freeze,
|
|
24
|
+
'<' => %w[<=> << <= <].freeze,
|
|
25
|
+
'>' => %w[>> >= >].freeze,
|
|
26
|
+
'+' => %w[+@ +].freeze,
|
|
27
|
+
'-' => %w[-@ -].freeze,
|
|
28
|
+
'*' => %w[** *].freeze,
|
|
29
|
+
'[' => ['[]=', '[]'].freeze,
|
|
30
|
+
'/' => ['/'].freeze,
|
|
31
|
+
'%' => ['%'].freeze,
|
|
32
|
+
'&' => ['&'].freeze,
|
|
33
|
+
'|' => ['|'].freeze,
|
|
34
|
+
'^' => ['^'].freeze,
|
|
35
|
+
'~' => ['~'].freeze
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
IDENTIFIER_START = /[a-zA-Z]/
|
|
39
|
+
IDENTIFIER_CONTINUE = /[a-zA-Z0-9_]/
|
|
40
|
+
|
|
41
|
+
SETTER_TERMINATORS = (WHITESPACE + [',', ')', '}']).freeze
|
|
42
|
+
|
|
20
43
|
def self.call(string)
|
|
21
44
|
new(string).__send__(:run)
|
|
22
45
|
end
|
|
@@ -31,7 +54,7 @@ module Mutant
|
|
|
31
54
|
#{token.display_location}
|
|
32
55
|
MESSAGE
|
|
33
56
|
end
|
|
34
|
-
end #
|
|
57
|
+
end # InvalidToken
|
|
35
58
|
end # Error
|
|
36
59
|
|
|
37
60
|
private_class_method :new
|
|
@@ -58,71 +81,123 @@ module Mutant
|
|
|
58
81
|
end
|
|
59
82
|
|
|
60
83
|
def consume
|
|
61
|
-
|
|
84
|
+
loop do
|
|
62
85
|
skip_whitespace
|
|
86
|
+
break unless next? && !instance_variable_defined?(:@error)
|
|
63
87
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
88
|
+
consume_structural \
|
|
89
|
+
|| consume_eq \
|
|
90
|
+
|| consume_operator \
|
|
91
|
+
|| consume_identifier \
|
|
92
|
+
|| consume_invalid
|
|
67
93
|
end
|
|
68
94
|
end
|
|
69
95
|
|
|
70
|
-
def
|
|
96
|
+
def consume_structural
|
|
97
|
+
char = peek
|
|
98
|
+
type = STRUCTURAL.fetch(char) { return }
|
|
71
99
|
start_position = @next_position
|
|
100
|
+
advance_position
|
|
101
|
+
@tokens << token(type:, start_position:)
|
|
102
|
+
end
|
|
72
103
|
|
|
73
|
-
|
|
104
|
+
def consume_eq
|
|
105
|
+
return unless peek.eql?('=')
|
|
106
|
+
|
|
107
|
+
start_position = @next_position
|
|
74
108
|
|
|
75
|
-
|
|
109
|
+
EQ_OPERATORS.each do |op|
|
|
110
|
+
next unless matches?(op)
|
|
111
|
+
|
|
112
|
+
advance_positions(op.length)
|
|
113
|
+
@tokens << token(type: :string, start_position:, value: op)
|
|
114
|
+
return true
|
|
115
|
+
end
|
|
76
116
|
|
|
77
117
|
advance_position
|
|
118
|
+
@tokens << token(type: :eq, start_position:)
|
|
119
|
+
end
|
|
78
120
|
|
|
79
|
-
|
|
121
|
+
def consume_operator
|
|
122
|
+
operators = OPERATORS_BY_START[peek] or return
|
|
123
|
+
match = operators.detect { |op| matches?(op) } or return
|
|
124
|
+
|
|
125
|
+
start_position = @next_position
|
|
126
|
+
advance_positions(match.length)
|
|
127
|
+
@tokens << token(type: :string, start_position:, value: match)
|
|
80
128
|
end
|
|
81
129
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
130
|
+
def consume_identifier
|
|
131
|
+
return unless IDENTIFIER_START.match?(peek)
|
|
132
|
+
|
|
133
|
+
start_position = @next_position
|
|
134
|
+
advance_position while IDENTIFIER_CONTINUE.match?(peek)
|
|
135
|
+
consume_identifier_suffix
|
|
136
|
+
|
|
137
|
+
@tokens << token(
|
|
138
|
+
type: :string,
|
|
139
|
+
start_position:,
|
|
140
|
+
value: @string[range_from(start_position)]
|
|
92
141
|
)
|
|
93
142
|
end
|
|
94
143
|
|
|
95
|
-
def
|
|
96
|
-
|
|
144
|
+
def consume_identifier_suffix
|
|
145
|
+
advance_position if suffix_char?(peek)
|
|
146
|
+
end
|
|
97
147
|
|
|
98
|
-
|
|
148
|
+
def suffix_char?(char)
|
|
149
|
+
char.eql?('!') || char.eql?('?') || (char.eql?('=') && setter_suffix_follows?)
|
|
150
|
+
end
|
|
99
151
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
end
|
|
152
|
+
def setter_suffix_follows?
|
|
153
|
+
next_char = @string[@next_position.succ]
|
|
154
|
+
|
|
155
|
+
next_char.nil? || SETTER_TERMINATORS.include?(next_char)
|
|
105
156
|
end
|
|
106
157
|
|
|
107
|
-
def
|
|
108
|
-
|
|
158
|
+
def consume_invalid
|
|
159
|
+
start_position = @next_position
|
|
160
|
+
advance_invalid
|
|
161
|
+
@error = Error::InvalidToken.new(
|
|
162
|
+
token: token(
|
|
163
|
+
type: :string,
|
|
164
|
+
start_position:,
|
|
165
|
+
value: @string[range_from(start_position)]
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
end
|
|
109
169
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
break
|
|
170
|
+
def advance_invalid
|
|
171
|
+
loop do
|
|
172
|
+
break unless next?
|
|
173
|
+
break if terminates_invalid?(peek)
|
|
113
174
|
|
|
114
|
-
string << char
|
|
115
175
|
advance_position
|
|
116
176
|
end
|
|
177
|
+
end
|
|
117
178
|
|
|
118
|
-
|
|
179
|
+
def terminates_invalid?(char)
|
|
180
|
+
STRUCTURAL.key?(char) || whitespace?(char)
|
|
119
181
|
end
|
|
120
182
|
|
|
121
|
-
def
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
183
|
+
def matches?(string)
|
|
184
|
+
@string[@next_position, string.length].eql?(string)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def advance_positions(count)
|
|
188
|
+
count.times { advance_position }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def token(type:, start_position:, value: nil)
|
|
192
|
+
Token.new(
|
|
193
|
+
type:,
|
|
194
|
+
value:,
|
|
195
|
+
location: Source::Location.new(
|
|
196
|
+
source: @source,
|
|
197
|
+
line_index: @line_index,
|
|
198
|
+
line_start: @line_start,
|
|
199
|
+
range: range_from(start_position)
|
|
200
|
+
)
|
|
126
201
|
)
|
|
127
202
|
end
|
|
128
203
|
|
|
@@ -130,10 +205,6 @@ module Mutant
|
|
|
130
205
|
start_position...@next_position
|
|
131
206
|
end
|
|
132
207
|
|
|
133
|
-
def valid_string?(string)
|
|
134
|
-
STRING_PATTERN.match?(string)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
208
|
def advance_position
|
|
138
209
|
@next_position += 1
|
|
139
210
|
end
|
|
@@ -165,6 +236,7 @@ module Mutant
|
|
|
165
236
|
@next_position < @string.length
|
|
166
237
|
end
|
|
167
238
|
end # Lexer
|
|
239
|
+
# rubocop:enable Metrics/ClassLength
|
|
168
240
|
end # Pattern
|
|
169
241
|
end # AST
|
|
170
242
|
end # Mutant
|
|
@@ -10,7 +10,7 @@ module Mutant
|
|
|
10
10
|
class Root < self
|
|
11
11
|
NAME = 'mutant'
|
|
12
12
|
SHORT_DESCRIPTION = 'mutation testing engine main command'
|
|
13
|
-
SUBCOMMANDS = [Environment::Run, Environment::Test::Run::Root, Environment, Util].freeze
|
|
13
|
+
SUBCOMMANDS = [Environment::Run, Environment::Test::Run::Root, Environment, Session, Util].freeze
|
|
14
14
|
end # Root
|
|
15
15
|
end # Command
|
|
16
16
|
end # CLI
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
module CLI
|
|
5
|
+
class Command
|
|
6
|
+
class Session < self
|
|
7
|
+
NAME = 'session'
|
|
8
|
+
SHORT_DESCRIPTION = 'Session history subcommands'
|
|
9
|
+
|
|
10
|
+
RESULTS_DIR = '.mutant/results'
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def session_files
|
|
15
|
+
dir = world.pathname.new(RESULTS_DIR)
|
|
16
|
+
|
|
17
|
+
return [] unless dir.directory?
|
|
18
|
+
|
|
19
|
+
dir.glob('*.json')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def load_session_file(path)
|
|
23
|
+
world.parse_json(path.read)
|
|
24
|
+
.bind(&Result::Session::CODEC.load_transform.public_method(:call))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Shared base for commands that operate on a session
|
|
28
|
+
class SessionCommand < self
|
|
29
|
+
OPTIONS = %i[add_session_id_option].freeze
|
|
30
|
+
|
|
31
|
+
UUID_FORMAT = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
|
|
32
|
+
|
|
33
|
+
private_constant(:UUID_FORMAT)
|
|
34
|
+
|
|
35
|
+
def display_config
|
|
36
|
+
@display_config || Reporter::CLI::Printer::DisplayConfig::DEFAULT
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def action
|
|
40
|
+
path = resolve_session_path or return Either::Left.new('No sessions found')
|
|
41
|
+
|
|
42
|
+
return Either::Left.new("Session file not found: #{path}") unless path.file?
|
|
43
|
+
|
|
44
|
+
load_session_file(path).either(
|
|
45
|
+
lambda { |error|
|
|
46
|
+
Either::Left.new("Failed to load session: #{error}\nRun `mutant session gc` to remove incompatible sessions.")
|
|
47
|
+
},
|
|
48
|
+
method(:run_report)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def add_session_id_option(parser)
|
|
55
|
+
parser.on('--session-id=ID', 'Session ID to operate on (default: latest)') do |value|
|
|
56
|
+
fail(OptionParser::InvalidArgument, "invalid UUID format: #{value}") unless UUID_FORMAT.match?(value)
|
|
57
|
+
|
|
58
|
+
@session_id = value
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def add_verbose_option(parser)
|
|
63
|
+
parser.separator("\nDisplay Options:\n\n")
|
|
64
|
+
parser.on('--verbose', 'Show verbose output') do
|
|
65
|
+
@display_config = Reporter::CLI::Printer::DisplayConfig::VERBOSE
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def resolve_session_path
|
|
70
|
+
if @session_id
|
|
71
|
+
world.pathname.new("#{RESULTS_DIR}/#{@session_id}.json")
|
|
72
|
+
else
|
|
73
|
+
session_files.last
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def run_report(session)
|
|
78
|
+
print("Session: #{session.session_id}")
|
|
79
|
+
|
|
80
|
+
print_report(session)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
abstract_method :print_report
|
|
84
|
+
end # SessionCommand
|
|
85
|
+
|
|
86
|
+
class List < self
|
|
87
|
+
NAME = 'list'
|
|
88
|
+
SHORT_DESCRIPTION = 'List past mutation testing sessions'
|
|
89
|
+
SUBCOMMANDS = [].freeze
|
|
90
|
+
|
|
91
|
+
HEADER_FORMAT = '%-6s %-10s %-8s %-10s %-10s %-36s %s'
|
|
92
|
+
ROW_FORMAT = '%-6s %-10s %-8s %-10s %-10s %-36s %s'
|
|
93
|
+
INCOMPATIBLE = '--------------- [incompatible] ---------------'
|
|
94
|
+
|
|
95
|
+
def action
|
|
96
|
+
print_header
|
|
97
|
+
session_files.reverse_each(&method(:print_session))
|
|
98
|
+
|
|
99
|
+
Either::Right.new(nil)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def print_header
|
|
105
|
+
print(HEADER_FORMAT % ['ALIVE', 'MUTATIONS', 'SUBJECTS', 'RUNTIME', 'KILLTIME', 'SESSION ID', 'TIMESTAMP'])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def print_session(path)
|
|
109
|
+
load_session_file(path).either(
|
|
110
|
+
->(_error) { print(colorize_unsupported(path)) },
|
|
111
|
+
method(:print_session_row)
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def print_session_row(session)
|
|
116
|
+
subjects = session.subject_results
|
|
117
|
+
|
|
118
|
+
print(ROW_FORMAT % [
|
|
119
|
+
subjects.sum(&:amount_mutations_alive),
|
|
120
|
+
subjects.sum(&:amount_mutations),
|
|
121
|
+
subjects.length,
|
|
122
|
+
format_time(session.runtime),
|
|
123
|
+
format_time(session.killtime),
|
|
124
|
+
session.session_id,
|
|
125
|
+
session.timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
|
126
|
+
])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def format_time(seconds)
|
|
130
|
+
'%.2fs' % seconds
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def colorize_unsupported(path)
|
|
134
|
+
session_id = path.basename('.json')
|
|
135
|
+
|
|
136
|
+
Unparser::Color::RED.format(INCOMPATIBLE.ljust(54)) + session_id.to_s
|
|
137
|
+
end
|
|
138
|
+
end # List
|
|
139
|
+
|
|
140
|
+
class Show < SessionCommand
|
|
141
|
+
include Mutant::Reporter::CLI::Printer::AliveResults
|
|
142
|
+
|
|
143
|
+
NAME = 'show'
|
|
144
|
+
SHORT_DESCRIPTION = 'Show results of a past session'
|
|
145
|
+
SUBCOMMANDS = [].freeze
|
|
146
|
+
OPTIONS = (superclass::OPTIONS + %i[add_verbose_option]).freeze
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def print_report(session)
|
|
151
|
+
failed = session.subject_results.reject(&:success?)
|
|
152
|
+
|
|
153
|
+
print("Time: #{session.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
154
|
+
print("Version: #{session.mutant_version}")
|
|
155
|
+
print("Ruby: #{session.ruby_version}")
|
|
156
|
+
print("Subjects: #{session.subject_results.length}")
|
|
157
|
+
print("Alive: #{failed.flat_map(&:uncovered_results).length}")
|
|
158
|
+
|
|
159
|
+
print_alive_results(failed)
|
|
160
|
+
|
|
161
|
+
Either::Right.new(nil)
|
|
162
|
+
end
|
|
163
|
+
end # Show
|
|
164
|
+
|
|
165
|
+
class Subject < SessionCommand
|
|
166
|
+
include Mutant::Reporter::CLI::Printer::AliveResults
|
|
167
|
+
|
|
168
|
+
NAME = 'subject'
|
|
169
|
+
SHORT_DESCRIPTION = 'List subjects or show alive mutations for a specific subject'
|
|
170
|
+
SUBCOMMANDS = [].freeze
|
|
171
|
+
OPTIONS = (superclass::OPTIONS + %i[add_verbose_option]).freeze
|
|
172
|
+
|
|
173
|
+
HEADER_FORMAT = '%-6s %-6s %s'
|
|
174
|
+
ROW_FORMAT = '%-6s %-6s %s'
|
|
175
|
+
|
|
176
|
+
def parse_remaining_arguments(arguments)
|
|
177
|
+
case arguments.length
|
|
178
|
+
when 0 then Either::Right.new(self)
|
|
179
|
+
when 1
|
|
180
|
+
@expression = Mutant::Util.one(arguments)
|
|
181
|
+
Either::Right.new(self)
|
|
182
|
+
else
|
|
183
|
+
Either::Left.new('Expected zero or one subject expression argument')
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def print_report(session)
|
|
190
|
+
if @expression
|
|
191
|
+
print_subject_detail(session)
|
|
192
|
+
else
|
|
193
|
+
print_subject_list(session)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def print_subject_list(session)
|
|
198
|
+
print(HEADER_FORMAT % %w[ALIVE TOTAL SUBJECT])
|
|
199
|
+
|
|
200
|
+
session.subject_results
|
|
201
|
+
.sort_by { |subject_result| -subject_result.uncovered_results.length }
|
|
202
|
+
.each(&method(:print_subject_row))
|
|
203
|
+
|
|
204
|
+
Either::Right.new(nil)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def print_subject_row(subject_result)
|
|
208
|
+
alive = subject_result.uncovered_results.length
|
|
209
|
+
total = subject_result.amount_mutations
|
|
210
|
+
|
|
211
|
+
print(ROW_FORMAT % [alive, total, subject_result.expression_syntax])
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def print_subject_detail(session)
|
|
215
|
+
subject_result = session.subject_results.detect do |subject_result|
|
|
216
|
+
subject_result.expression_syntax.eql?(@expression)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
return Either::Left.new("Subject not found: #{@expression}") unless subject_result
|
|
220
|
+
|
|
221
|
+
print_alive_results([subject_result])
|
|
222
|
+
|
|
223
|
+
Either::Right.new(nil)
|
|
224
|
+
end
|
|
225
|
+
end # Subject
|
|
226
|
+
|
|
227
|
+
class GC < self
|
|
228
|
+
NAME = 'gc'
|
|
229
|
+
SHORT_DESCRIPTION = 'Remove incompatible and old session results'
|
|
230
|
+
SUBCOMMANDS = [].freeze
|
|
231
|
+
OPTIONS = %i[add_gc_options].freeze
|
|
232
|
+
|
|
233
|
+
DEFAULT_KEEP = 100
|
|
234
|
+
|
|
235
|
+
def initialize(*)
|
|
236
|
+
super
|
|
237
|
+
@keep = DEFAULT_KEEP
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def action
|
|
241
|
+
incompatible, compatible = partition_sessions
|
|
242
|
+
|
|
243
|
+
incompatible.each(&:delete)
|
|
244
|
+
|
|
245
|
+
excess = compatible.length > @keep ? compatible.first(compatible.length - @keep) : []
|
|
246
|
+
excess.each(&:delete)
|
|
247
|
+
|
|
248
|
+
print("Removed #{incompatible.length + excess.length} session(s)")
|
|
249
|
+
|
|
250
|
+
Either::Right.new(nil)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
def add_gc_options(parser)
|
|
256
|
+
parser.on('--keep=N', Integer, "Keep N most recent sessions (default: #{DEFAULT_KEEP})") do |value|
|
|
257
|
+
@keep = value
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def partition_sessions
|
|
262
|
+
incompatible = []
|
|
263
|
+
compatible = []
|
|
264
|
+
|
|
265
|
+
session_files.each do |path|
|
|
266
|
+
load_session_file(path).either(
|
|
267
|
+
->(_error) { incompatible << path },
|
|
268
|
+
->(_session) { compatible << path }
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
[incompatible, compatible]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
end # GC
|
|
276
|
+
|
|
277
|
+
SUBCOMMANDS = [List, Show, Subject, GC].freeze
|
|
278
|
+
end # Session
|
|
279
|
+
end # Command
|
|
280
|
+
end # CLI
|
|
281
|
+
end # Mutant
|
data/lib/mutant/cli/command.rb
CHANGED
|
@@ -220,8 +220,22 @@ module Mutant
|
|
|
220
220
|
"#{full_name}: #{message}\n\n#{parser}"
|
|
221
221
|
end
|
|
222
222
|
|
|
223
|
-
def
|
|
224
|
-
world.stdout
|
|
223
|
+
def output
|
|
224
|
+
world.stdout
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def puts(message)
|
|
228
|
+
output.puts(message)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
alias_method :print, :puts
|
|
232
|
+
|
|
233
|
+
def parse_remaining_arguments(arguments)
|
|
234
|
+
if arguments.empty?
|
|
235
|
+
Either::Right.new(self)
|
|
236
|
+
else
|
|
237
|
+
Either::Left.new("unexpected arguments: #{arguments.join(' ')}")
|
|
238
|
+
end
|
|
225
239
|
end
|
|
226
240
|
end # Command
|
|
227
241
|
# rubocop:enable Metrics/ClassLength
|
data/lib/mutant/config.rb
CHANGED
|
@@ -120,7 +120,7 @@ module Mutant
|
|
|
120
120
|
.lmap { |exception| "MUTANT_JOBS environment variable has invalid value: #{jobs.inspect} - #{exception}" }
|
|
121
121
|
.fmap { |jobs_value| DEFAULT.with(jobs: jobs_value) }
|
|
122
122
|
else
|
|
123
|
-
Either::Right.new(DEFAULT)
|
|
123
|
+
Either::Right.new(DEFAULT.with(coverage_criteria: CoverageCriteria::EMPTY))
|
|
124
124
|
end
|
|
125
125
|
end
|
|
126
126
|
private_class_method :load_env_config
|
|
@@ -89,7 +89,7 @@ module Mutant
|
|
|
89
89
|
def result
|
|
90
90
|
Result.new(
|
|
91
91
|
exception: @exception,
|
|
92
|
-
log: @log_fragments.join,
|
|
92
|
+
log: LogCapture.from_binary(@log_fragments.join),
|
|
93
93
|
process_status: @process_status,
|
|
94
94
|
timeout: @timeout,
|
|
95
95
|
value: @value
|
|
@@ -132,11 +132,7 @@ module Mutant
|
|
|
132
132
|
def load_result(result_fragments)
|
|
133
133
|
@value = world.marshal.load(result_fragments.join)
|
|
134
134
|
rescue ArgumentError => exception
|
|
135
|
-
@exception = Exception.
|
|
136
|
-
backtrace: exception.backtrace,
|
|
137
|
-
message: exception.message,
|
|
138
|
-
original_class: exception.class
|
|
139
|
-
)
|
|
135
|
+
@exception = Mutant::Result::Exception.from_exception(exception)
|
|
140
136
|
end
|
|
141
137
|
|
|
142
138
|
# rubocop:disable Metrics/MethodLength
|
|
@@ -197,7 +193,7 @@ module Mutant
|
|
|
197
193
|
end
|
|
198
194
|
|
|
199
195
|
def handle_status(status)
|
|
200
|
-
@process_status = status
|
|
196
|
+
@process_status = Mutant::Result::ProcessStatus.from_process_status(status)
|
|
201
197
|
end
|
|
202
198
|
|
|
203
199
|
def peek_child
|