mutant 0.9.12 → 0.10.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of mutant might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/bin/mutant +16 -11
- data/lib/mutant.rb +7 -4
- data/lib/mutant/bootstrap.rb +14 -1
- data/lib/mutant/cli.rb +9 -162
- 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 +8 -7
- data/lib/mutant/matcher/config.rb +2 -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/isolation_result.rb +9 -3
- 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
- metadata +15 -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,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.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
|
@@ -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,13 @@ 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
|
+
.to_set
|
50
|
+
)
|
50
51
|
end
|
51
52
|
|
52
53
|
def apply(world)
|
@@ -59,10 +60,10 @@ module Mutant
|
|
59
60
|
private
|
60
61
|
|
61
62
|
def check_subscription(actual)
|
62
|
-
if (
|
63
|
+
if (licensed & actual).any?
|
63
64
|
success
|
64
65
|
else
|
65
|
-
failure(
|
66
|
+
failure(licensed, actual)
|
66
67
|
end
|
67
68
|
end
|
68
69
|
|
@@ -7,6 +7,7 @@ module Mutant
|
|
7
7
|
include Adamantium, Anima.new(
|
8
8
|
:ignore_expressions,
|
9
9
|
:match_expressions,
|
10
|
+
:start_expressions,
|
10
11
|
:subject_filters
|
11
12
|
)
|
12
13
|
|
@@ -18,6 +19,7 @@ module Mutant
|
|
18
19
|
PRESENTATIONS = IceNine.deep_freeze(
|
19
20
|
ignore_expressions: :syntax,
|
20
21
|
match_expressions: :syntax,
|
22
|
+
start_expressions: :syntax,
|
21
23
|
subject_filters: :inspect
|
22
24
|
)
|
23
25
|
private_constant(*constants(false))
|
data/lib/mutant/meta/example.rb
CHANGED
@@ -3,7 +3,19 @@
|
|
3
3
|
module Mutant
|
4
4
|
module Meta
|
5
5
|
class Example
|
6
|
-
include Adamantium
|
6
|
+
include Adamantium
|
7
|
+
include Anima.new(
|
8
|
+
:expected,
|
9
|
+
:file,
|
10
|
+
:lvars,
|
11
|
+
:node,
|
12
|
+
:original_source,
|
13
|
+
:types
|
14
|
+
)
|
15
|
+
|
16
|
+
class Expected
|
17
|
+
include Anima.new(:original_source, :node)
|
18
|
+
end
|
7
19
|
|
8
20
|
# Verification instance for example
|
9
21
|
#
|
@@ -13,13 +25,13 @@ module Mutant
|
|
13
25
|
end
|
14
26
|
memoize :verification
|
15
27
|
|
16
|
-
#
|
28
|
+
# Original source as generated by unparser
|
17
29
|
#
|
18
30
|
# @return [String]
|
19
|
-
def
|
31
|
+
def original_source_generated
|
20
32
|
Unparser.unparse(node)
|
21
33
|
end
|
22
|
-
memoize :
|
34
|
+
memoize :original_source_generated
|
23
35
|
|
24
36
|
# Generated mutations on example source
|
25
37
|
#
|