mutant 0.9.14 → 0.10.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/bin/mutant +16 -11
- data/lib/mutant.rb +6 -2
- data/lib/mutant/cli.rb +8 -167
- data/lib/mutant/cli/command.rb +196 -0
- data/lib/mutant/cli/command/root.rb +13 -0
- data/lib/mutant/cli/command/run.rb +151 -0
- data/lib/mutant/cli/command/subscription.rb +54 -0
- data/lib/mutant/expression.rb +0 -1
- data/lib/mutant/license.rb +9 -35
- data/lib/mutant/license/subscription.rb +31 -9
- data/lib/mutant/license/subscription/commercial.rb +2 -4
- data/lib/mutant/license/subscription/opensource.rb +7 -7
- data/lib/mutant/mutator/node/named_value/variable_assignment.rb +1 -1
- data/lib/mutant/version.rb +1 -1
- metadata +14 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd38b14761b4836ca3ce54a9bd8ea7a25d1f4dac6506b08ab6f6c64227a0f71d
|
4
|
+
data.tar.gz: dbed2538feb3240195e76b8b10895ea01ba7dc827820a6106d00fa4a55da97d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c70d6881956ace0d359b13dcfd41ef4eb92dbabcf5ee90d913790b8a09bb1b12877e32c85453d3be97d9fd2a1523a0af307ebf7318550cd0e3413fc8d9ecb765
|
7
|
+
data.tar.gz: 27b59ee208993bfc5b128269658fc4465782052613b0641c9647ace34cc10b956c72c8e05a5b8a6f86af77112589295e3289e53192d973500fe43ceed84180fc
|
data/bin/mutant
CHANGED
@@ -8,8 +8,14 @@ end
|
|
8
8
|
|
9
9
|
require 'mutant'
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
command = Mutant::CLI.parse(
|
12
|
+
arguments: ARGV,
|
13
|
+
config: Mutant::Config::DEFAULT,
|
14
|
+
world: Mutant::WORLD
|
15
|
+
)
|
16
|
+
|
17
|
+
status =
|
18
|
+
if command.zombie?
|
13
19
|
$stderr.puts('Running mutant zombified!')
|
14
20
|
Mutant::Zombifier.call(
|
15
21
|
namespace: :Zombie,
|
@@ -31,15 +37,14 @@ namespace =
|
|
31
37
|
concord
|
32
38
|
]
|
33
39
|
)
|
34
|
-
|
40
|
+
|
41
|
+
Zombie::Mutant::CLI.parse(
|
42
|
+
arguments: ARGV,
|
43
|
+
config: Zombie::Mutant::Config::DEFAULT,
|
44
|
+
world: Zombie::Mutant::WORLD
|
45
|
+
).call
|
35
46
|
else
|
36
|
-
|
47
|
+
command.call
|
37
48
|
end
|
38
49
|
|
39
|
-
Kernel.exit(
|
40
|
-
namespace::CLI.run(
|
41
|
-
namespace::WORLD,
|
42
|
-
namespace::Config::DEFAULT,
|
43
|
-
ARGV
|
44
|
-
)
|
45
|
-
)
|
50
|
+
Kernel.exit(status)
|
data/lib/mutant.rb
CHANGED
@@ -165,6 +165,10 @@ require 'mutant/selector/expression'
|
|
165
165
|
require 'mutant/selector/null'
|
166
166
|
require 'mutant/config'
|
167
167
|
require 'mutant/cli'
|
168
|
+
require 'mutant/cli/command'
|
169
|
+
require 'mutant/cli/command/run'
|
170
|
+
require 'mutant/cli/command/subscription'
|
171
|
+
require 'mutant/cli/command/root'
|
168
172
|
require 'mutant/runner'
|
169
173
|
require 'mutant/runner/sink'
|
170
174
|
require 'mutant/result'
|
@@ -211,8 +215,8 @@ module Mutant
|
|
211
215
|
open3: Open3,
|
212
216
|
pathname: Pathname,
|
213
217
|
process: Process,
|
214
|
-
stderr:
|
215
|
-
stdout:
|
218
|
+
stderr: $stderr,
|
219
|
+
stdout: $stdout,
|
216
220
|
thread: Thread,
|
217
221
|
warnings: Warnings.new(Warning)
|
218
222
|
)
|
data/lib/mutant/cli.rb
CHANGED
@@ -1,174 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Mutant
|
4
|
-
# Commandline
|
5
|
-
|
6
|
-
|
7
|
-
class CLI
|
8
|
-
include Concord.new(:world, :config)
|
9
|
-
|
10
|
-
private_class_method :new
|
11
|
-
|
12
|
-
OPTIONS =
|
13
|
-
%i[
|
14
|
-
add_environment_options
|
15
|
-
add_mutation_options
|
16
|
-
add_filter_options
|
17
|
-
add_debug_options
|
18
|
-
].freeze
|
19
|
-
|
20
|
-
private_constant(*constants(false))
|
21
|
-
|
22
|
-
# Run cli with arguments
|
23
|
-
#
|
24
|
-
# @param [World] world
|
25
|
-
# the outside world
|
26
|
-
#
|
27
|
-
# @param [Config] default_config
|
28
|
-
# the default config
|
29
|
-
#
|
30
|
-
# @param [Array<String>]
|
31
|
-
# the user provided arguments
|
32
|
-
#
|
33
|
-
# @return [Boolean]
|
34
|
-
#
|
35
|
-
# rubocop:disable Style/Semicolon
|
36
|
-
#
|
37
|
-
# ignore :reek:LongParameterList
|
38
|
-
def self.run(world, default_config, arguments)
|
39
|
-
License
|
40
|
-
.apply(world)
|
41
|
-
.bind { Config.load_config_file(world, default_config) }
|
42
|
-
.bind { |file_config| apply(world, file_config, arguments) }
|
43
|
-
.bind { |cli_config| Bootstrap.apply(world, cli_config) }
|
44
|
-
.bind(&Runner.method(:apply))
|
45
|
-
.from_right { |error| world.stderr.puts(error); return false }
|
46
|
-
.success?
|
47
|
-
end
|
48
|
-
# rubocop:enable Style/Semicolon
|
49
|
-
|
50
|
-
# Parse arguments into config
|
51
|
-
#
|
52
|
-
# @param [World] world
|
53
|
-
# @param [Config] config
|
54
|
-
# @param [Array<String>] arguments
|
55
|
-
#
|
56
|
-
# @return [Either<OptionParser::ParseError, Config>]
|
57
|
-
#
|
58
|
-
# ignore :reek:LongParameterList
|
59
|
-
def self.apply(world, config, arguments)
|
60
|
-
Either
|
61
|
-
.wrap_error(OptionParser::ParseError) { new(world, config).parse(arguments) }
|
62
|
-
.lmap(&:message)
|
63
|
-
end
|
64
|
-
|
65
|
-
# Local opt out of option parser defaults
|
66
|
-
class OptionParser < ::OptionParser
|
67
|
-
# Kill defaults added by option parser that
|
68
|
-
# inference with ours under mutation testing.
|
69
|
-
define_method(:add_officious) {}
|
70
|
-
end # OptionParser
|
71
|
-
|
72
|
-
# Parse the command-line options
|
4
|
+
# Commandline interface
|
5
|
+
module CLI
|
6
|
+
# Parse command
|
73
7
|
#
|
74
|
-
# @
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
opts = OptionParser.new do |builder|
|
80
|
-
builder.banner = 'usage: mutant [options] MATCH_EXPRESSION ...'
|
81
|
-
OPTIONS.each do |name|
|
82
|
-
__send__(name, builder)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
parse_match_expressions(opts.parse!(arguments.dup))
|
87
|
-
|
88
|
-
config
|
89
|
-
end
|
90
|
-
|
91
|
-
private
|
92
|
-
|
93
|
-
def parse_match_expressions(expressions)
|
94
|
-
expressions.each do |expression|
|
95
|
-
add_matcher(:match_expressions, config.expression_parser.apply(expression).from_right)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# rubocop:disable Metrics/MethodLength
|
100
|
-
def add_environment_options(opts)
|
101
|
-
opts.separator('Environment:')
|
102
|
-
opts.on('--zombie', 'Run mutant zombified') do
|
103
|
-
with(zombie: true)
|
104
|
-
end
|
105
|
-
opts.on('-I', '--include DIRECTORY', 'Add DIRECTORY to $LOAD_PATH') do |directory|
|
106
|
-
add(:includes, directory)
|
107
|
-
end
|
108
|
-
opts.on('-r', '--require NAME', 'Require file with NAME') do |name|
|
109
|
-
add(:requires, name)
|
110
|
-
end
|
111
|
-
opts.on('-j', '--jobs NUMBER', 'Number of kill jobs. Defaults to number of processors.') do |number|
|
112
|
-
with(jobs: Integer(number))
|
113
|
-
end
|
114
|
-
end
|
115
|
-
# rubocop:enable Metrics/MethodLength
|
116
|
-
|
117
|
-
def add_mutation_options(opts)
|
118
|
-
opts.separator(nil)
|
119
|
-
opts.separator('Options:')
|
120
|
-
|
121
|
-
opts.on('--use INTEGRATION', 'Use INTEGRATION to kill mutations') do |name|
|
122
|
-
with(integration: name)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# rubocop:disable Metrics/MethodLength
|
127
|
-
def add_filter_options(opts)
|
128
|
-
opts.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
|
129
|
-
add_matcher(:ignore_expressions, config.expression_parser.apply(pattern).from_right)
|
130
|
-
end
|
131
|
-
opts.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
|
132
|
-
add_matcher(:start_expressions, config.expression_parser.apply(pattern).from_right)
|
133
|
-
end
|
134
|
-
opts.on('--since REVISION', 'Only select subjects touched since REVISION') do |revision|
|
135
|
-
add_matcher(
|
136
|
-
:subject_filters,
|
137
|
-
Repository::SubjectFilter.new(
|
138
|
-
Repository::Diff.new(to: revision, world: world)
|
139
|
-
)
|
140
|
-
)
|
141
|
-
end
|
142
|
-
end
|
143
|
-
# rubocop:enable Metrics/MethodLength
|
144
|
-
|
145
|
-
# rubocop:disable Metrics/MethodLength
|
146
|
-
def add_debug_options(opts)
|
147
|
-
opts.on('--fail-fast', 'Fail fast') do
|
148
|
-
with(fail_fast: true)
|
149
|
-
end
|
150
|
-
opts.on('--version', 'Print mutants version') do
|
151
|
-
world.stdout.puts("mutant-#{VERSION}")
|
152
|
-
world.kernel.exit
|
153
|
-
end
|
154
|
-
opts.on_tail('-h', '--help', 'Show this message') do
|
155
|
-
world.stdout.puts(opts.to_s)
|
156
|
-
world.kernel.exit
|
157
|
-
end
|
158
|
-
end
|
159
|
-
# rubocop:enable Metrics/MethodLength
|
160
|
-
|
161
|
-
def with(attributes)
|
162
|
-
@config = config.with(attributes)
|
163
|
-
end
|
164
|
-
|
165
|
-
def add(attribute, value)
|
166
|
-
with(attribute => config.public_send(attribute) + [value])
|
167
|
-
end
|
168
|
-
|
169
|
-
def add_matcher(attribute, value)
|
170
|
-
with(matcher: config.matcher.add(attribute, value))
|
8
|
+
# @return [Command]
|
9
|
+
def self.parse(world:, **attributes)
|
10
|
+
Command::Root
|
11
|
+
.parse(world: world, **attributes)
|
12
|
+
.from_right { |message| Command::FailParse.new(world, message) }
|
171
13
|
end
|
172
14
|
end # CLI
|
173
|
-
# rubocop:enable Metrics/ClassLength
|
174
15
|
end # Mutant
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
module CLI
|
5
|
+
# rubocop:disable Metrics/ClassLength
|
6
|
+
class Command
|
7
|
+
include AbstractType, Anima.new(:world, :config, :main, :parent, :arguments)
|
8
|
+
|
9
|
+
include Equalizer.new(:parent, :arguments)
|
10
|
+
|
11
|
+
OPTIONS = [].freeze
|
12
|
+
SUBCOMMANDS = [].freeze
|
13
|
+
|
14
|
+
# Local opt out of option parser defaults
|
15
|
+
class OptionParser < ::OptionParser
|
16
|
+
# Kill defaults added by option parser that
|
17
|
+
# inference with ours under mutation testing.
|
18
|
+
define_method(:add_officious) {}
|
19
|
+
end # OptionParser
|
20
|
+
|
21
|
+
class FailParse < self
|
22
|
+
include Concord.new(:world, :message)
|
23
|
+
|
24
|
+
def call
|
25
|
+
world.stderr.puts(message)
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Parse command
|
31
|
+
#
|
32
|
+
# @return [Command]
|
33
|
+
def self.parse(**attributes)
|
34
|
+
new(main: nil, parent: nil, **attributes).__send__(:parse)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Command name
|
38
|
+
#
|
39
|
+
# @return [String]
|
40
|
+
def self.command_name
|
41
|
+
self::NAME
|
42
|
+
end
|
43
|
+
|
44
|
+
# Command short description
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
def self.short_description
|
48
|
+
self::SHORT_DESCRIPTION
|
49
|
+
end
|
50
|
+
|
51
|
+
# Execute the command, invoke its side effects
|
52
|
+
#
|
53
|
+
# @return [Bool]
|
54
|
+
def call
|
55
|
+
main ? main.call : execute
|
56
|
+
end
|
57
|
+
|
58
|
+
# Commands full name
|
59
|
+
#
|
60
|
+
# @return [String]
|
61
|
+
def full_name
|
62
|
+
[*parent&.full_name, self.class.command_name].join(' ')
|
63
|
+
end
|
64
|
+
|
65
|
+
# Test if command needs to be executed in zombie environment
|
66
|
+
#
|
67
|
+
# @return [Bool]
|
68
|
+
def zombie?
|
69
|
+
instance_of?(Run)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def subcommands
|
75
|
+
self.class::SUBCOMMANDS
|
76
|
+
end
|
77
|
+
|
78
|
+
def parser
|
79
|
+
OptionParser.new do |parser|
|
80
|
+
parser.banner = "usage: #{banner}"
|
81
|
+
|
82
|
+
add_summary(parser)
|
83
|
+
add_global_options(parser)
|
84
|
+
add_subcommands(parser)
|
85
|
+
|
86
|
+
self.class::OPTIONS.each do |method_name|
|
87
|
+
2.times { parser.separator(nil) }
|
88
|
+
__send__(method_name, parser)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def capture_main(&block)
|
94
|
+
@main = block
|
95
|
+
end
|
96
|
+
|
97
|
+
def banner
|
98
|
+
if subcommands.any?
|
99
|
+
"#{full_name} <#{subcommands.map(&:command_name).join('|')}> [options]"
|
100
|
+
else
|
101
|
+
"#{full_name} [options]"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse
|
106
|
+
Either
|
107
|
+
.wrap_error(OptionParser::InvalidOption) { parser.order(arguments) }
|
108
|
+
.lmap { |error| "#{full_name}: #{error}" }
|
109
|
+
.bind(&method(:parse_remaining))
|
110
|
+
end
|
111
|
+
|
112
|
+
def add_summary(parser)
|
113
|
+
parser.separator(nil)
|
114
|
+
parser.separator("Summary: #{self.class.short_description}")
|
115
|
+
parser.separator(nil)
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_global_options(parser)
|
119
|
+
parser.separator('Global Options:')
|
120
|
+
parser.separator(nil)
|
121
|
+
|
122
|
+
parser.on('--help', 'Print help') do
|
123
|
+
capture_main { world.stdout.puts(parser.help); true }
|
124
|
+
end
|
125
|
+
|
126
|
+
parser.on('--version', 'Print mutants version') do
|
127
|
+
capture_main { world.stdout.puts("mutant-#{VERSION}"); true }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def add_subcommands(parser)
|
132
|
+
return unless subcommands.any?
|
133
|
+
|
134
|
+
parser.separator(nil)
|
135
|
+
parser.separator('Available subcommands:')
|
136
|
+
parser.separator(nil)
|
137
|
+
parser.separator(format_subcommands)
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_remaining(remaining)
|
141
|
+
return Either::Right.new(self) if main
|
142
|
+
|
143
|
+
if subcommands.any?
|
144
|
+
parse_subcommand(remaining)
|
145
|
+
else
|
146
|
+
parse_remaining_arguments(remaining)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def parse_remaining_arguments(remaining)
|
151
|
+
if remaining.any?
|
152
|
+
Either::Left.new("#{full_name}: Does not expect extra arguments")
|
153
|
+
else
|
154
|
+
Either::Right.new(self)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def parse_subcommand(arguments)
|
159
|
+
command_name, *arguments = arguments
|
160
|
+
|
161
|
+
if command_name.nil?
|
162
|
+
Either::Left.new(
|
163
|
+
"Missing required subcommand!\n\n#{parser}"
|
164
|
+
)
|
165
|
+
else
|
166
|
+
find_command(command_name).bind do |command|
|
167
|
+
command.parse(**to_h, parent: self, arguments: arguments)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def format_subcommands
|
173
|
+
commands = subcommands.map do |subcommand|
|
174
|
+
[subcommand.command_name, subcommand.short_description]
|
175
|
+
end.to_h
|
176
|
+
|
177
|
+
width = commands.each_key.map(&:length).max
|
178
|
+
|
179
|
+
commands.each_key.map do |name|
|
180
|
+
'%-*s - %s' % [width, name, commands.fetch(name)] # rubocop:disable Style/FormatStringToken
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def find_command(name)
|
185
|
+
subcommand = subcommands.detect { |command| command.command_name.eql?(name) }
|
186
|
+
|
187
|
+
if subcommand
|
188
|
+
Either::Right.new(subcommand)
|
189
|
+
else
|
190
|
+
Either::Left.new("#{full_name}: Cannot find subcommand #{name.inspect}")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end # Command
|
194
|
+
# rubocop:enable Metrics/ClassLength
|
195
|
+
end # CLI
|
196
|
+
end # Mutant
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
module CLI
|
5
|
+
class Command
|
6
|
+
class Root < self
|
7
|
+
SUBCOMMANDS = [Run, Subscription].freeze
|
8
|
+
SHORT_DESCRIPTION = 'mutation testing engine main command'
|
9
|
+
NAME = 'mutant'
|
10
|
+
end
|
11
|
+
end # Command
|
12
|
+
end # CLI
|
13
|
+
end # Mutant
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
module CLI
|
5
|
+
class Command
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
7
|
+
class Run < self
|
8
|
+
NAME = 'run'
|
9
|
+
SHORT_DESCRIPTION = 'Run code analysis'
|
10
|
+
|
11
|
+
OPTIONS =
|
12
|
+
%i[
|
13
|
+
add_environment_options
|
14
|
+
add_runner_options
|
15
|
+
add_integration_options
|
16
|
+
add_matcher_options
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
SLEEP = 40
|
20
|
+
|
21
|
+
UNLICENSED = <<~MESSAGE.lines.freeze
|
22
|
+
Soft fail, continuing in #{SLEEP} seconds
|
23
|
+
Next major version will enforce the license
|
24
|
+
See https://github.com/mbj/mutant#licensing
|
25
|
+
MESSAGE
|
26
|
+
|
27
|
+
# Test if command needs to be executed in zombie environment
|
28
|
+
#
|
29
|
+
# @return [Bool]
|
30
|
+
def zombie?
|
31
|
+
config.zombie
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def execute
|
37
|
+
soft_fail(License.apply(world))
|
38
|
+
.bind { Config.load_config_file(world, config) }
|
39
|
+
.bind { |cli_config| Bootstrap.apply(world, cli_config) }
|
40
|
+
.bind(&Runner.public_method(:apply))
|
41
|
+
.from_right { |error| world.stderr.puts(error); return false }
|
42
|
+
.success?
|
43
|
+
end
|
44
|
+
|
45
|
+
def soft_fail(result)
|
46
|
+
result.either(
|
47
|
+
lambda do |message|
|
48
|
+
stderr = world.stderr
|
49
|
+
stderr.puts(message)
|
50
|
+
UNLICENSED.each { |line| stderr.puts(unlicensed(line)) }
|
51
|
+
world.kernel.sleep(SLEEP)
|
52
|
+
Either::Right.new(nil)
|
53
|
+
end,
|
54
|
+
->(_subscription) { Either::Right.new(nil) }
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def unlicensed(message)
|
59
|
+
"[Mutant-License-Error]: #{message}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_remaining_arguments(arguments)
|
63
|
+
traverse(config.expression_parser.public_method(:apply), arguments)
|
64
|
+
.fmap do |match_expressions|
|
65
|
+
matcher(match_expressions: match_expressions)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def traverse(action, values)
|
71
|
+
Either::Right.new(
|
72
|
+
values.map do |value|
|
73
|
+
action.call(value).from_right do |error|
|
74
|
+
return Either::Left.new(error)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def set(**attributes)
|
81
|
+
@config = config.with(attributes)
|
82
|
+
end
|
83
|
+
|
84
|
+
def matcher(**attributes)
|
85
|
+
set(matcher: config.matcher.with(attributes))
|
86
|
+
end
|
87
|
+
|
88
|
+
def add(attribute, value)
|
89
|
+
set(attribute => config.public_send(attribute) + [value])
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_matcher(attribute, value)
|
93
|
+
set(matcher: config.matcher.add(attribute, value))
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_environment_options(parser)
|
97
|
+
parser.separator('Environment:')
|
98
|
+
parser.on('--zombie', 'Run mutant zombified') do
|
99
|
+
set(zombie: true)
|
100
|
+
end
|
101
|
+
parser.on('-I', '--include DIRECTORY', 'Add DIRECTORY to $LOAD_PATH') do |directory|
|
102
|
+
add(:includes, directory)
|
103
|
+
end
|
104
|
+
parser.on('-r', '--require NAME', 'Require file with NAME') do |name|
|
105
|
+
add(:requires, name)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_integration_options(parser)
|
110
|
+
parser.separator('Integration:')
|
111
|
+
|
112
|
+
parser.on('--use INTEGRATION', 'Use INTEGRATION to kill mutations') do |name|
|
113
|
+
set(integration: name)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# rubocop:disable Metrics/MethodLength
|
118
|
+
def add_matcher_options(parser)
|
119
|
+
parser.separator('Matcher:')
|
120
|
+
|
121
|
+
parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
|
122
|
+
add_matcher(:ignore_expressions, config.expression_parser.apply(pattern).from_right)
|
123
|
+
end
|
124
|
+
parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
|
125
|
+
add_matcher(:start_expressions, config.expression_parser.apply(pattern).from_right)
|
126
|
+
end
|
127
|
+
parser.on('--since REVISION', 'Only select subjects touched since REVISION') do |revision|
|
128
|
+
add_matcher(
|
129
|
+
:subject_filters,
|
130
|
+
Repository::SubjectFilter.new(
|
131
|
+
Repository::Diff.new(to: revision, world: world)
|
132
|
+
)
|
133
|
+
)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def add_runner_options(parser)
|
138
|
+
parser.separator('Runner:')
|
139
|
+
|
140
|
+
parser.on('--fail-fast', 'Fail fast') do
|
141
|
+
set(fail_fast: true)
|
142
|
+
end
|
143
|
+
parser.on('-j', '--jobs NUMBER', 'Number of kill jobs. Defaults to number of processors.') do |number|
|
144
|
+
set(jobs: Integer(number))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end # Run
|
148
|
+
# rubocop:enable Metrics/ClassLength
|
149
|
+
end # Command
|
150
|
+
end # CLI
|
151
|
+
end # Mutant
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
module CLI
|
5
|
+
class Command
|
6
|
+
class Subscription < self
|
7
|
+
NAME = 'subscription'
|
8
|
+
SHORT_DESCRIPTION = 'Subscription subcommands'
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def license
|
13
|
+
License.apply(world)
|
14
|
+
end
|
15
|
+
|
16
|
+
class Test < self
|
17
|
+
NAME = 'test'
|
18
|
+
SUBCOMMANDS = [].freeze
|
19
|
+
SHORT_DESCRIPTION = 'Silently validates subscription, exits accordingly'
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def execute
|
24
|
+
license.right?
|
25
|
+
end
|
26
|
+
end # Test
|
27
|
+
|
28
|
+
class Show < self
|
29
|
+
NAME = 'show'
|
30
|
+
SUBCOMMANDS = [].freeze
|
31
|
+
SHORT_DESCRIPTION = 'Show subscription status'
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def execute
|
36
|
+
license.either(method(:unlicensed), method(:licensed))
|
37
|
+
end
|
38
|
+
|
39
|
+
def licensed(subscription)
|
40
|
+
world.stdout.puts(subscription.description)
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def unlicensed(message)
|
45
|
+
world.stderr.puts(message)
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end # Show
|
49
|
+
|
50
|
+
SUBCOMMANDS = [Show, Test].freeze
|
51
|
+
end # Subscription
|
52
|
+
end # Command
|
53
|
+
end # CLI
|
54
|
+
end # Mutant
|
data/lib/mutant/expression.rb
CHANGED
data/lib/mutant/license.rb
CHANGED
@@ -4,53 +4,27 @@ module Mutant
|
|
4
4
|
module License
|
5
5
|
NAME = 'mutant-license'
|
6
6
|
VERSION = '~> 0.1.0'
|
7
|
-
SLEEP = 40
|
8
|
-
|
9
|
-
UNLICENSED =
|
10
|
-
IceNine.deep_freeze(
|
11
|
-
[
|
12
|
-
"Soft fail, continuing in #{SLEEP} seconds",
|
13
|
-
'Next major version will enforce the license',
|
14
|
-
'See https://github.com/mbj/mutant#licensing'
|
15
|
-
]
|
16
|
-
)
|
17
7
|
|
8
|
+
# Load license
|
9
|
+
#
|
10
|
+
# @param [World] world
|
11
|
+
#
|
12
|
+
# @return [Either<String,Subscription>]
|
13
|
+
#
|
14
|
+
# @api private
|
18
15
|
def self.apply(world)
|
19
|
-
soft_fail(world, license_result(world))
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.license_result(world)
|
23
16
|
load_mutant_license(world)
|
24
17
|
.fmap { license_path(world) }
|
25
|
-
.
|
26
|
-
.bind { |sub| sub.apply(world) }
|
18
|
+
.bind { |path| Subscription.load(world, world.json.load(path)) }
|
27
19
|
end
|
28
|
-
private_class_method :license_result
|
29
|
-
|
30
|
-
# ignore :reek:NestedIterators
|
31
|
-
def self.soft_fail(world, result)
|
32
|
-
result.lmap do |message|
|
33
|
-
stderr = world.stderr
|
34
|
-
stderr.puts(message)
|
35
|
-
UNLICENSED.each { |line| stderr.puts(unlicensed(line)) }
|
36
|
-
world.kernel.sleep(SLEEP)
|
37
|
-
end
|
38
|
-
|
39
|
-
Either::Right.new(true)
|
40
|
-
end
|
41
|
-
private_class_method :soft_fail
|
42
20
|
|
43
21
|
def self.load_mutant_license(world)
|
44
22
|
Either
|
45
23
|
.wrap_error(LoadError) { world.gem_method.call(NAME, VERSION) }
|
46
24
|
.lmap(&:message)
|
47
25
|
.lmap(&method(:check_for_rubygems_mutant_license))
|
48
|
-
.lmap(&method(:unlicensed))
|
49
|
-
end
|
50
|
-
|
51
|
-
def self.unlicensed(message)
|
52
|
-
"[Mutant-License-Error]: #{message}"
|
53
26
|
end
|
27
|
+
private_class_method :load_mutant_license
|
54
28
|
|
55
29
|
def self.check_for_rubygems_mutant_license(message)
|
56
30
|
if message.include?('already activated mutant-license-0.0.0')
|
@@ -3,8 +3,15 @@
|
|
3
3
|
module Mutant
|
4
4
|
module License
|
5
5
|
class Subscription
|
6
|
+
include Concord.new(:licensed)
|
6
7
|
|
7
|
-
|
8
|
+
FORMAT = <<~'MESSAGE'
|
9
|
+
%<subscription_name>s subscription:
|
10
|
+
Licensed:
|
11
|
+
%<licensed>s
|
12
|
+
MESSAGE
|
13
|
+
|
14
|
+
FAILURE_FORMAT = <<~'MESSAGE'
|
8
15
|
Can not validate %<subscription_name>s license.
|
9
16
|
Licensed:
|
10
17
|
%<expected>s
|
@@ -12,31 +19,46 @@ module Mutant
|
|
12
19
|
%<actual>s
|
13
20
|
MESSAGE
|
14
21
|
|
15
|
-
|
22
|
+
# Load value into subscription
|
23
|
+
#
|
24
|
+
# @param [Object] value
|
25
|
+
#
|
26
|
+
# @return [Subscription]
|
27
|
+
def self.load(world, value)
|
16
28
|
{
|
17
29
|
'com' => Commercial,
|
18
30
|
'oss' => Opensource
|
19
|
-
}.fetch(value.fetch('type'))
|
31
|
+
}.fetch(value.fetch('type'))
|
32
|
+
.from_json(value.fetch('contents'))
|
33
|
+
.apply(world)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Subscription self description
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
def description
|
40
|
+
FORMAT % {
|
41
|
+
licensed: licensed.join("\n"),
|
42
|
+
subscription_name: subscription_name
|
43
|
+
}
|
20
44
|
end
|
21
45
|
|
22
46
|
private
|
23
47
|
|
24
48
|
def failure(expected, actual)
|
25
|
-
Either::Left.new(
|
49
|
+
Either::Left.new(failure_message(expected, actual))
|
26
50
|
end
|
27
51
|
|
28
|
-
# ignore :reek:UtilityFunction
|
29
52
|
def success
|
30
|
-
|
31
|
-
Either::Right.new(nil)
|
53
|
+
Either::Right.new(self)
|
32
54
|
end
|
33
55
|
|
34
56
|
def subscription_name
|
35
57
|
self.class.name.split('::').last.downcase
|
36
58
|
end
|
37
59
|
|
38
|
-
def
|
39
|
-
|
60
|
+
def failure_message(expected, actual)
|
61
|
+
FAILURE_FORMAT % {
|
40
62
|
actual: actual.any? ? actual.map(&:to_s).join("\n") : '[none]',
|
41
63
|
expected: expected.map(&:to_s).join("\n"),
|
42
64
|
subscription_name: subscription_name
|
@@ -4,8 +4,6 @@ module Mutant
|
|
4
4
|
module License
|
5
5
|
class Subscription
|
6
6
|
class Commercial < self
|
7
|
-
include Concord.new(:authors)
|
8
|
-
|
9
7
|
class Author
|
10
8
|
include Concord.new(:email)
|
11
9
|
|
@@ -20,10 +18,10 @@ module Mutant
|
|
20
18
|
def apply(world)
|
21
19
|
candidates = candidates(world)
|
22
20
|
|
23
|
-
if (
|
21
|
+
if (licensed & candidates).any?
|
24
22
|
success
|
25
23
|
else
|
26
|
-
failure(
|
24
|
+
failure(licensed, candidates)
|
27
25
|
end
|
28
26
|
end
|
29
27
|
|
@@ -4,8 +4,6 @@ module Mutant
|
|
4
4
|
module License
|
5
5
|
class Subscription
|
6
6
|
class Opensource < self
|
7
|
-
include Concord.new(:repositories)
|
8
|
-
|
9
7
|
class Repository
|
10
8
|
include Concord.new(:host, :path)
|
11
9
|
|
@@ -43,10 +41,12 @@ module Mutant
|
|
43
41
|
private_class_method :parse_url
|
44
42
|
end
|
45
43
|
|
46
|
-
private_constant(*constants(false))
|
47
|
-
|
48
44
|
def self.from_json(value)
|
49
|
-
new(
|
45
|
+
new(
|
46
|
+
value
|
47
|
+
.fetch('repositories')
|
48
|
+
.map(&Repository.public_method(:parse))
|
49
|
+
)
|
50
50
|
end
|
51
51
|
|
52
52
|
def apply(world)
|
@@ -59,10 +59,10 @@ module Mutant
|
|
59
59
|
private
|
60
60
|
|
61
61
|
def check_subscription(actual)
|
62
|
-
if (
|
62
|
+
if (licensed.to_set & actual).any?
|
63
63
|
success
|
64
64
|
else
|
65
|
-
failure(
|
65
|
+
failure(licensed, actual)
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
data/lib/mutant/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mutant
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Markus Schirp
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-10-
|
11
|
+
date: 2020-10-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: abstract_type
|
@@ -184,14 +184,14 @@ dependencies:
|
|
184
184
|
requirements:
|
185
185
|
- - "~>"
|
186
186
|
- !ruby/object:Gem::Version
|
187
|
-
version: 0.5.
|
187
|
+
version: 0.5.3
|
188
188
|
type: :runtime
|
189
189
|
prerelease: false
|
190
190
|
version_requirements: !ruby/object:Gem::Requirement
|
191
191
|
requirements:
|
192
192
|
- - "~>"
|
193
193
|
- !ruby/object:Gem::Version
|
194
|
-
version: 0.5.
|
194
|
+
version: 0.5.3
|
195
195
|
- !ruby/object:Gem::Dependency
|
196
196
|
name: variable
|
197
197
|
requirement: !ruby/object:Gem::Requirement
|
@@ -254,28 +254,28 @@ dependencies:
|
|
254
254
|
requirements:
|
255
255
|
- - "~>"
|
256
256
|
- !ruby/object:Gem::Version
|
257
|
-
version: 1.
|
257
|
+
version: 1.3.0
|
258
258
|
type: :development
|
259
259
|
prerelease: false
|
260
260
|
version_requirements: !ruby/object:Gem::Requirement
|
261
261
|
requirements:
|
262
262
|
- - "~>"
|
263
263
|
- !ruby/object:Gem::Version
|
264
|
-
version: 1.
|
264
|
+
version: 1.3.0
|
265
265
|
- !ruby/object:Gem::Dependency
|
266
266
|
name: rubocop
|
267
267
|
requirement: !ruby/object:Gem::Requirement
|
268
268
|
requirements:
|
269
269
|
- - "~>"
|
270
270
|
- !ruby/object:Gem::Version
|
271
|
-
version:
|
271
|
+
version: '1.0'
|
272
272
|
type: :development
|
273
273
|
prerelease: false
|
274
274
|
version_requirements: !ruby/object:Gem::Requirement
|
275
275
|
requirements:
|
276
276
|
- - "~>"
|
277
277
|
- !ruby/object:Gem::Version
|
278
|
-
version:
|
278
|
+
version: '1.0'
|
279
279
|
description: Mutation Testing for Ruby.
|
280
280
|
email:
|
281
281
|
- mbj@schirp-dso.com
|
@@ -303,6 +303,10 @@ files:
|
|
303
303
|
- lib/mutant/ast/types.rb
|
304
304
|
- lib/mutant/bootstrap.rb
|
305
305
|
- lib/mutant/cli.rb
|
306
|
+
- lib/mutant/cli/command.rb
|
307
|
+
- lib/mutant/cli/command/root.rb
|
308
|
+
- lib/mutant/cli/command/run.rb
|
309
|
+
- lib/mutant/cli/command/subscription.rb
|
306
310
|
- lib/mutant/config.rb
|
307
311
|
- lib/mutant/context.rb
|
308
312
|
- lib/mutant/env.rb
|
@@ -460,14 +464,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
460
464
|
requirements:
|
461
465
|
- - ">="
|
462
466
|
- !ruby/object:Gem::Version
|
463
|
-
version: '
|
467
|
+
version: '2.5'
|
464
468
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
465
469
|
requirements:
|
466
470
|
- - ">="
|
467
471
|
- !ruby/object:Gem::Version
|
468
472
|
version: '0'
|
469
473
|
requirements: []
|
470
|
-
rubygems_version: 3.1
|
474
|
+
rubygems_version: 3.2.0.rc.1
|
471
475
|
signing_key:
|
472
476
|
specification_version: 4
|
473
477
|
summary: ''
|