mutant 0.9.13 → 0.10.5
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 +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
|