mutant 0.9.13 → 0.10.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/mutant +14 -11
- data/lib/mutant.rb +10 -6
- 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 +161 -0
- data/lib/mutant/cli/command/subscription.rb +54 -0
- data/lib/mutant/config.rb +28 -52
- 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 +8 -7
- data/lib/mutant/matcher/config.rb +13 -0
- data/lib/mutant/meta/example.rb +16 -4
- data/lib/mutant/meta/example/dsl.rb +33 -16
- data/lib/mutant/meta/example/verification.rb +70 -28
- data/lib/mutant/mutator/node.rb +2 -2
- data/lib/mutant/mutator/node/{dstr.rb → dynamic_literal.rb} +7 -5
- data/lib/mutant/mutator/node/index.rb +4 -4
- data/lib/mutant/mutator/node/named_value/variable_assignment.rb +1 -1
- data/lib/mutant/mutator/node/op_asgn.rb +15 -1
- data/lib/mutant/mutator/node/send.rb +1 -1
- data/lib/mutant/mutator/node/send/attribute_assignment.rb +1 -0
- data/lib/mutant/reporter/cli/printer/config.rb +2 -2
- data/lib/mutant/selector/expression.rb +3 -1
- data/lib/mutant/subject/method/instance.rb +1 -1
- data/lib/mutant/test.rb +1 -1
- data/lib/mutant/version.rb +1 -1
- data/lib/mutant/world.rb +52 -0
- metadata +16 -13
- data/lib/mutant/minitest/coverage.rb +0 -53
- data/lib/mutant/mutator/node/dsym.rb +0 -22
@@ -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,161 @@
|
|
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 initialize(attributes)
|
37
|
+
super(attributes)
|
38
|
+
@config = Config::DEFAULT
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute
|
42
|
+
soft_fail(License.apply(world))
|
43
|
+
.bind { Config.load_config_file(world) }
|
44
|
+
.fmap(&method(:expand))
|
45
|
+
.bind { Bootstrap.apply(world, @config) }
|
46
|
+
.bind(&Runner.public_method(:apply))
|
47
|
+
.from_right { |error| world.stderr.puts(error); return false }
|
48
|
+
.success?
|
49
|
+
end
|
50
|
+
|
51
|
+
def expand(file_config)
|
52
|
+
@config = Config.env.merge(file_config).merge(@config)
|
53
|
+
end
|
54
|
+
|
55
|
+
def soft_fail(result)
|
56
|
+
result.either(
|
57
|
+
lambda do |message|
|
58
|
+
stderr = world.stderr
|
59
|
+
stderr.puts(message)
|
60
|
+
UNLICENSED.each { |line| stderr.puts(unlicensed(line)) }
|
61
|
+
world.kernel.sleep(SLEEP)
|
62
|
+
Either::Right.new(nil)
|
63
|
+
end,
|
64
|
+
->(_subscription) { Either::Right.new(nil) }
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
def unlicensed(message)
|
69
|
+
"[Mutant-License-Error]: #{message}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_remaining_arguments(arguments)
|
73
|
+
traverse(@config.expression_parser.public_method(:apply), arguments)
|
74
|
+
.fmap do |match_expressions|
|
75
|
+
matcher(match_expressions: match_expressions)
|
76
|
+
self
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def traverse(action, values)
|
81
|
+
Either::Right.new(
|
82
|
+
values.map do |value|
|
83
|
+
action.call(value).from_right do |error|
|
84
|
+
return Either::Left.new(error)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
def set(**attributes)
|
91
|
+
@config = @config.with(attributes)
|
92
|
+
end
|
93
|
+
|
94
|
+
def matcher(**attributes)
|
95
|
+
set(matcher: @config.matcher.with(attributes))
|
96
|
+
end
|
97
|
+
|
98
|
+
def add(attribute, value)
|
99
|
+
set(attribute => @config.public_send(attribute) + [value])
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_matcher(attribute, value)
|
103
|
+
set(matcher: @config.matcher.add(attribute, value))
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_environment_options(parser)
|
107
|
+
parser.separator('Environment:')
|
108
|
+
parser.on('--zombie', 'Run mutant zombified') do
|
109
|
+
set(zombie: true)
|
110
|
+
end
|
111
|
+
parser.on('-I', '--include DIRECTORY', 'Add DIRECTORY to $LOAD_PATH') do |directory|
|
112
|
+
add(:includes, directory)
|
113
|
+
end
|
114
|
+
parser.on('-r', '--require NAME', 'Require file with NAME') do |name|
|
115
|
+
add(:requires, name)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_integration_options(parser)
|
120
|
+
parser.separator('Integration:')
|
121
|
+
|
122
|
+
parser.on('--use INTEGRATION', 'Use INTEGRATION to kill mutations') do |name|
|
123
|
+
set(integration: name)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# rubocop:disable Metrics/MethodLength
|
128
|
+
def add_matcher_options(parser)
|
129
|
+
parser.separator('Matcher:')
|
130
|
+
|
131
|
+
parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
|
132
|
+
add_matcher(:ignore_expressions, @config.expression_parser.apply(pattern).from_right)
|
133
|
+
end
|
134
|
+
parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
|
135
|
+
add_matcher(:start_expressions, @config.expression_parser.apply(pattern).from_right)
|
136
|
+
end
|
137
|
+
parser.on('--since REVISION', 'Only select subjects touched since REVISION') do |revision|
|
138
|
+
add_matcher(
|
139
|
+
:subject_filters,
|
140
|
+
Repository::SubjectFilter.new(
|
141
|
+
Repository::Diff.new(to: revision, world: world)
|
142
|
+
)
|
143
|
+
)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def add_runner_options(parser)
|
148
|
+
parser.separator('Runner:')
|
149
|
+
|
150
|
+
parser.on('--fail-fast', 'Fail fast') do
|
151
|
+
set(fail_fast: true)
|
152
|
+
end
|
153
|
+
parser.on('-j', '--jobs NUMBER', 'Number of kill jobs. Defaults to number of processors.') do |number|
|
154
|
+
set(jobs: Integer(number))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end # Run
|
158
|
+
# rubocop:enable Metrics/ClassLength
|
159
|
+
end # Command
|
160
|
+
end # CLI
|
161
|
+
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/config.rb
CHANGED
@@ -1,55 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Mutant
|
4
|
-
# The outer world IO objects mutant does interact with
|
5
|
-
class World
|
6
|
-
include Adamantium::Flat, Anima.new(
|
7
|
-
:condition_variable,
|
8
|
-
:gem,
|
9
|
-
:gem_method,
|
10
|
-
:io,
|
11
|
-
:json,
|
12
|
-
:kernel,
|
13
|
-
:load_path,
|
14
|
-
:marshal,
|
15
|
-
:mutex,
|
16
|
-
:object_space,
|
17
|
-
:open3,
|
18
|
-
:pathname,
|
19
|
-
:process,
|
20
|
-
:stderr,
|
21
|
-
:stdout,
|
22
|
-
:thread,
|
23
|
-
:warnings
|
24
|
-
)
|
25
|
-
|
26
|
-
INSPECT = '#<Mutant::World>'
|
27
|
-
|
28
|
-
private_constant(*constants(false))
|
29
|
-
|
30
|
-
# Object inspection
|
31
|
-
#
|
32
|
-
# @return [String]
|
33
|
-
def inspect
|
34
|
-
INSPECT
|
35
|
-
end
|
36
|
-
|
37
|
-
# Capture stdout of a command
|
38
|
-
#
|
39
|
-
# @param [Array<String>] command
|
40
|
-
#
|
41
|
-
# @return [Either<String,String>]
|
42
|
-
def capture_stdout(command)
|
43
|
-
stdout, status = open3.capture2(*command, binmode: true)
|
44
|
-
|
45
|
-
if status.success?
|
46
|
-
Either::Right.new(stdout)
|
47
|
-
else
|
48
|
-
Either::Left.new("Command #{command} failed!")
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end # World
|
52
|
-
|
53
4
|
# Standalone configuration of a mutant execution.
|
54
5
|
#
|
55
6
|
# Does not reference any "external" volatile state. The configuration applied
|
@@ -106,6 +57,23 @@ module Mutant
|
|
106
57
|
mutant.yml
|
107
58
|
].freeze
|
108
59
|
|
60
|
+
# Merge with other config
|
61
|
+
#
|
62
|
+
# @param [Config] other
|
63
|
+
#
|
64
|
+
# @return [Config]
|
65
|
+
def merge(other)
|
66
|
+
other.with(
|
67
|
+
fail_fast: fail_fast || other.fail_fast,
|
68
|
+
includes: includes + other.includes,
|
69
|
+
jobs: other.jobs || jobs,
|
70
|
+
integration: other.integration || integration,
|
71
|
+
matcher: matcher.merge(other.matcher),
|
72
|
+
requires: requires + other.requires,
|
73
|
+
zombie: zombie || other.zombie
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
109
77
|
private_constant(*constants(false))
|
110
78
|
|
111
79
|
# Load config file
|
@@ -114,11 +82,12 @@ module Mutant
|
|
114
82
|
# @param [Config] config
|
115
83
|
#
|
116
84
|
# @return [Either<String,Config>]
|
117
|
-
def self.load_config_file(world
|
118
|
-
|
85
|
+
def self.load_config_file(world)
|
86
|
+
config = DEFAULT
|
87
|
+
files = CANDIDATES.map(&world.pathname.public_method(:new)).select(&:readable?)
|
119
88
|
|
120
89
|
if files.one?
|
121
|
-
load_contents(files.first).fmap(&config.
|
90
|
+
load_contents(files.first).fmap(&config.public_method(:with))
|
122
91
|
elsif files.empty?
|
123
92
|
Either::Right.new(config)
|
124
93
|
else
|
@@ -133,5 +102,12 @@ module Mutant
|
|
133
102
|
.lmap(&:compact_message)
|
134
103
|
end
|
135
104
|
private_class_method :load_contents
|
105
|
+
|
106
|
+
# The configuration from the environment
|
107
|
+
#
|
108
|
+
# @return [Config]
|
109
|
+
def self.env
|
110
|
+
DEFAULT.with(jobs: Etc.nprocessors)
|
111
|
+
end
|
136
112
|
end # Config
|
137
113
|
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.to_a.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
|