mutant 0.8.22 → 0.8.23
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Changelog.md +5 -0
- data/lib/mutant.rb +5 -4
- data/lib/mutant/env.rb +17 -17
- data/lib/mutant/isolation.rb +42 -1
- data/lib/mutant/isolation/fork.rb +118 -54
- data/lib/mutant/isolation/none.rb +4 -7
- data/lib/mutant/loader.rb +2 -2
- data/lib/mutant/mutation.rb +9 -8
- data/lib/mutant/reporter/cli/printer/isolation_result.rb +112 -0
- data/lib/mutant/reporter/cli/printer/mutation_result.rb +5 -7
- data/lib/mutant/result.rb +20 -12
- data/lib/mutant/runner/sink.rb +2 -2
- data/lib/mutant/version.rb +1 -1
- data/spec/support/shared_context.rb +34 -15
- data/spec/unit/mutant/env_spec.rb +64 -59
- data/spec/unit/mutant/isolation/fork_spec.rb +164 -70
- data/spec/unit/mutant/isolation/none_spec.rb +12 -7
- data/spec/unit/mutant/isolation/result_spec.rb +41 -0
- data/spec/unit/mutant/loader_spec.rb +1 -5
- data/spec/unit/mutant/mutation_spec.rb +4 -3
- data/spec/unit/mutant/reporter/cli/printer/isolation_result_spec.rb +124 -0
- data/spec/unit/mutant/reporter/cli/printer/mutation_result_spec.rb +21 -11
- data/spec/unit/mutant/result/env_spec.rb +51 -4
- data/spec/unit/mutant/result/mutation_spec.rb +40 -9
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da1d34448bf320507db377b41c46f591022e69e02597f4097a9a8ff52cf7b7a8
|
4
|
+
data.tar.gz: fd4a4c3d03d89ec53e5e7a040c0f3e4854d58d2bd1347404f06cae2541db742c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e64e8ed9f28ee391b704aa37d18c4ec204ad3c73af56e3fed7d4430399bea663a599a0561d8e567b7d4fa9416b53ba810a5df95529c47345dfe18781a2367be0
|
7
|
+
data.tar.gz: be1ec3a98dfbf5b55dbc8543cf61a0dcf6a187232d0fd3d640ddc31533aaa1ed658a433f36bdc1158eb81df4774da13df737a8c59dd4e331369045957da7d226
|
data/Changelog.md
CHANGED
data/lib/mutant.rb
CHANGED
@@ -195,14 +195,15 @@ require 'mutant/reporter/sequence'
|
|
195
195
|
require 'mutant/reporter/cli'
|
196
196
|
require 'mutant/reporter/cli/printer'
|
197
197
|
require 'mutant/reporter/cli/printer/config'
|
198
|
-
require 'mutant/reporter/cli/printer/env_result'
|
199
198
|
require 'mutant/reporter/cli/printer/env_progress'
|
200
|
-
require 'mutant/reporter/cli/printer/
|
199
|
+
require 'mutant/reporter/cli/printer/env_result'
|
200
|
+
require 'mutant/reporter/cli/printer/isolation_result'
|
201
201
|
require 'mutant/reporter/cli/printer/mutation_progress_result'
|
202
|
-
require 'mutant/reporter/cli/printer/
|
203
|
-
require 'mutant/reporter/cli/printer/subject_result'
|
202
|
+
require 'mutant/reporter/cli/printer/mutation_result'
|
204
203
|
require 'mutant/reporter/cli/printer/status'
|
205
204
|
require 'mutant/reporter/cli/printer/status_progressive'
|
205
|
+
require 'mutant/reporter/cli/printer/subject_progress'
|
206
|
+
require 'mutant/reporter/cli/printer/subject_result'
|
206
207
|
require 'mutant/reporter/cli/printer/test_result'
|
207
208
|
require 'mutant/reporter/cli/tput'
|
208
209
|
require 'mutant/reporter/cli/format'
|
data/lib/mutant/env.rb
CHANGED
@@ -24,13 +24,25 @@ module Mutant
|
|
24
24
|
#
|
25
25
|
# @return [Result::Mutation]
|
26
26
|
def kill(mutation)
|
27
|
-
|
27
|
+
start = Timer.now
|
28
|
+
|
28
29
|
Result::Mutation.new(
|
29
|
-
|
30
|
-
|
30
|
+
isolation_result: run_mutation_tests(mutation),
|
31
|
+
mutation: mutation,
|
32
|
+
runtime: Timer.now - start
|
31
33
|
)
|
32
34
|
end
|
33
35
|
|
36
|
+
# The test selections
|
37
|
+
#
|
38
|
+
# @return Hash{Mutation => Enumerable<Test>}
|
39
|
+
def selections
|
40
|
+
subjects.map do |subject|
|
41
|
+
[subject, selector.call(subject)]
|
42
|
+
end.to_h
|
43
|
+
end
|
44
|
+
memoize :selections
|
45
|
+
|
34
46
|
private
|
35
47
|
|
36
48
|
# Kill mutation under isolation with integration
|
@@ -38,24 +50,12 @@ module Mutant
|
|
38
50
|
# @param [Isolation] isolation
|
39
51
|
# @param [Integration] integration
|
40
52
|
#
|
41
|
-
# @return [Result::
|
42
|
-
#
|
43
|
-
# rubocop:disable MethodLength
|
53
|
+
# @return [Result::Isolation]
|
44
54
|
def run_mutation_tests(mutation)
|
45
|
-
start = Timer.now
|
46
|
-
tests = selector.call(mutation.subject)
|
47
|
-
|
48
55
|
config.isolation.call do
|
49
56
|
mutation.insert(config.kernel)
|
50
|
-
integration.call(
|
57
|
+
integration.call(selections.fetch(mutation.subject))
|
51
58
|
end
|
52
|
-
rescue Isolation::Error => error
|
53
|
-
Result::Test.new(
|
54
|
-
output: error.message,
|
55
|
-
passed: false,
|
56
|
-
runtime: Timer.now - start,
|
57
|
-
tests: tests
|
58
|
-
)
|
59
59
|
end
|
60
60
|
|
61
61
|
end # Env
|
data/lib/mutant/isolation.rb
CHANGED
@@ -1,12 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Mutant
|
4
|
+
# Isolation mechanism
|
4
5
|
class Isolation
|
5
6
|
include AbstractType
|
6
7
|
|
8
|
+
# Isolated computation result
|
9
|
+
class Result
|
10
|
+
include AbstractType, Adamantium
|
11
|
+
|
12
|
+
abstract_method :error
|
13
|
+
abstract_method :next
|
14
|
+
abstract_method :value
|
15
|
+
|
16
|
+
# Add error on top of current result
|
17
|
+
#
|
18
|
+
# @param [Result] error
|
19
|
+
#
|
20
|
+
# @return [Result]
|
21
|
+
def add_error(error)
|
22
|
+
ErrorChain.new(error, self)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Test for success
|
26
|
+
#
|
27
|
+
# @return [Boolean]
|
28
|
+
def success?
|
29
|
+
instance_of?(Success)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Succesful result producing value
|
33
|
+
class Success < self
|
34
|
+
include Concord::Public.new(:value)
|
35
|
+
end # Success
|
36
|
+
|
37
|
+
# Unsuccessful result by unexpected exception
|
38
|
+
class Exception < self
|
39
|
+
include Concord::Public.new(:value)
|
40
|
+
end # Error
|
41
|
+
|
42
|
+
# Result when there where many results
|
43
|
+
class ErrorChain < Result
|
44
|
+
include Concord::Public.new(:value, :next)
|
45
|
+
end # ChainError
|
46
|
+
end # Result
|
47
|
+
|
7
48
|
# Call block in isolation
|
8
49
|
#
|
9
|
-
# @return [
|
50
|
+
# @return [Result]
|
10
51
|
# the blocks result
|
11
52
|
abstract_method :call
|
12
53
|
end # Isolation
|
@@ -3,72 +3,136 @@
|
|
3
3
|
module Mutant
|
4
4
|
class Isolation
|
5
5
|
# Isolation via the fork(2) systemcall.
|
6
|
-
#
|
7
|
-
# We do inject so many globals and common patterns to make this unit
|
8
|
-
# specifiable without mocking the globals and more important: Not having
|
9
|
-
# mutations that bypass mocks into a real world side effect.
|
10
6
|
class Fork < self
|
11
|
-
include
|
7
|
+
include(
|
8
|
+
Adamantium::Flat,
|
9
|
+
Anima.new(:devnull, :io, :marshal, :process, :stderr, :stdout)
|
10
|
+
)
|
12
11
|
|
13
|
-
|
14
|
-
undef_method :fork
|
12
|
+
ATTRIBUTES = (anima.attribute_names + %i[block reader writer]).freeze
|
15
13
|
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
14
|
+
# Unsucessful result as child exited nonzero
|
15
|
+
class ChildError < Result
|
16
|
+
include Concord::Public.new(:value)
|
17
|
+
end # ChildError
|
18
|
+
|
19
|
+
# Unsucessful result as fork failed
|
20
|
+
class ForkError < Result
|
21
|
+
include Equalizer.new
|
22
|
+
end # ForkError
|
23
|
+
|
24
|
+
# ignore :reek:InstanceVariableAssumption
|
25
|
+
class Parent
|
26
|
+
include(
|
27
|
+
Anima.new(*ATTRIBUTES),
|
28
|
+
Procto.call
|
29
|
+
)
|
30
|
+
|
31
|
+
# Prevent mutation from `process.fork` to `fork` to call Kernel#fork
|
32
|
+
undef_method :fork
|
33
|
+
|
34
|
+
# Parent process
|
35
|
+
#
|
36
|
+
# @param [IO] reader
|
37
|
+
# @param [IO] writer
|
38
|
+
#
|
39
|
+
# @return [Result]
|
40
|
+
def call
|
41
|
+
pid = start_child or return ForkError.new
|
42
|
+
|
43
|
+
read_child_result(pid)
|
44
|
+
|
45
|
+
@result
|
26
46
|
end
|
27
|
-
rescue => exception
|
28
|
-
raise Error, exception
|
29
|
-
end
|
30
47
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
pid = process.fork do
|
39
|
-
child(reader, writer, &block)
|
48
|
+
private
|
49
|
+
|
50
|
+
# Start child process
|
51
|
+
#
|
52
|
+
# @return [Integer]
|
53
|
+
def start_child
|
54
|
+
process.fork { Child.call(to_h) }
|
40
55
|
end
|
41
56
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
57
|
+
# Read child result
|
58
|
+
#
|
59
|
+
# @param [Integer] pid
|
60
|
+
#
|
61
|
+
# @return [undefined]
|
62
|
+
def read_child_result(pid)
|
63
|
+
writer.close
|
47
64
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
65
|
+
add_result(Result::Success.new(marshal.load(reader)))
|
66
|
+
rescue ArgumentError, EOFError => exception
|
67
|
+
add_result(Result::Exception.new(exception))
|
68
|
+
ensure
|
69
|
+
wait_child(pid)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Wait for child process
|
73
|
+
#
|
74
|
+
# @param [Integer] pid
|
75
|
+
#
|
76
|
+
# @return [undefined]
|
77
|
+
def wait_child(pid)
|
78
|
+
_pid, status = process.wait2(pid)
|
79
|
+
|
80
|
+
add_result(ChildError.new(status)) unless status.success?
|
81
|
+
end
|
60
82
|
|
61
|
-
|
83
|
+
# Add a result
|
84
|
+
#
|
85
|
+
# @param [Result]
|
86
|
+
def add_result(result)
|
87
|
+
@result = defined?(@result) ? @result.add_error(result) : result
|
88
|
+
end
|
89
|
+
end # Parent
|
90
|
+
|
91
|
+
class Child
|
92
|
+
include(
|
93
|
+
Adamantium::Flat,
|
94
|
+
Anima.new(*ATTRIBUTES),
|
95
|
+
Procto.call
|
96
|
+
)
|
97
|
+
|
98
|
+
# Handle child process
|
99
|
+
#
|
100
|
+
# @param [IO] reader
|
101
|
+
# @param [IO] writer
|
102
|
+
#
|
103
|
+
# @return [undefined]
|
104
|
+
def call
|
105
|
+
reader.close
|
106
|
+
writer.binmode
|
107
|
+
writer.syswrite(marshal.dump(result(&block)))
|
108
|
+
writer.close
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# The block result computed under silencing
|
114
|
+
#
|
115
|
+
# @return [Object]
|
116
|
+
def result
|
117
|
+
devnull.call do |null|
|
118
|
+
stderr.reopen(null)
|
119
|
+
stdout.reopen(null)
|
120
|
+
yield
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end # Child
|
124
|
+
|
125
|
+
private_constant(*(constants(false) - %i[ChildError ForkError]))
|
126
|
+
|
127
|
+
# Call block in isolation
|
62
128
|
#
|
63
|
-
# @return [
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
yield
|
129
|
+
# @return [Result]
|
130
|
+
# execution result
|
131
|
+
def call(&block)
|
132
|
+
io.pipe(binmode: true) do |(reader, writer)|
|
133
|
+
Parent.call(to_h.merge(block: block, reader: reader, writer: writer))
|
69
134
|
end
|
70
135
|
end
|
71
|
-
|
72
136
|
end # Fork
|
73
137
|
end # Isolation
|
74
138
|
end # Mutant
|
@@ -3,8 +3,6 @@
|
|
3
3
|
module Mutant
|
4
4
|
# Module providing isolation
|
5
5
|
class Isolation
|
6
|
-
Error = Class.new(RuntimeError)
|
7
|
-
|
8
6
|
# Absolutly no isolation
|
9
7
|
#
|
10
8
|
# Only useful for debugging.
|
@@ -12,14 +10,13 @@ module Mutant
|
|
12
10
|
|
13
11
|
# Call block in no isolation
|
14
12
|
#
|
15
|
-
# @return [
|
13
|
+
# @return [Result]
|
16
14
|
#
|
17
|
-
#
|
18
|
-
# if block terminates abnormal
|
15
|
+
# ignore :reek:UtilityFunction
|
19
16
|
def call
|
20
|
-
yield
|
17
|
+
Result::Success.new(yield)
|
21
18
|
rescue => exception
|
22
|
-
|
19
|
+
Result::Exception.new(exception)
|
23
20
|
end
|
24
21
|
|
25
22
|
end # None
|
data/lib/mutant/loader.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Mutant
|
4
4
|
# Base class for code loaders
|
5
5
|
class Loader
|
6
|
-
include Anima.new(:binding, :kernel, :
|
6
|
+
include Anima.new(:binding, :kernel, :source, :subject)
|
7
7
|
|
8
8
|
# Call loader
|
9
9
|
#
|
@@ -19,7 +19,7 @@ module Mutant
|
|
19
19
|
# @return [undefined]
|
20
20
|
def call
|
21
21
|
kernel.eval(
|
22
|
-
|
22
|
+
source,
|
23
23
|
binding,
|
24
24
|
subject.source_path.to_s,
|
25
25
|
subject.source_line
|
data/lib/mutant/mutation.rb
CHANGED
@@ -33,6 +33,14 @@ module Mutant
|
|
33
33
|
end
|
34
34
|
memoize :source
|
35
35
|
|
36
|
+
# The monkeypatch to insert the mutation
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
def monkeypatch
|
40
|
+
Unparser.unparse(subject.context.root(node))
|
41
|
+
end
|
42
|
+
memoize :monkeypatch
|
43
|
+
|
36
44
|
# Normalized original source
|
37
45
|
#
|
38
46
|
# @return [String]
|
@@ -59,7 +67,7 @@ module Mutant
|
|
59
67
|
Loader.call(
|
60
68
|
binding: TOPLEVEL_BINDING,
|
61
69
|
kernel: kernel,
|
62
|
-
|
70
|
+
source: monkeypatch,
|
63
71
|
subject: subject
|
64
72
|
)
|
65
73
|
self
|
@@ -75,13 +83,6 @@ module Mutant
|
|
75
83
|
end
|
76
84
|
memoize :sha1
|
77
85
|
|
78
|
-
# Mutated root node
|
79
|
-
#
|
80
|
-
# @return [Parser::AST::Node]
|
81
|
-
def root
|
82
|
-
subject.context.root(node)
|
83
|
-
end
|
84
|
-
|
85
86
|
# Evil mutation that should case mutations to fail tests
|
86
87
|
class Evil < self
|
87
88
|
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Reporter
|
5
|
+
class CLI
|
6
|
+
class Printer
|
7
|
+
# Reporter for mutation results
|
8
|
+
#
|
9
|
+
# :reek:TooManyConstants
|
10
|
+
class IsolationResult < self
|
11
|
+
CHILD_ERROR_MESSAGE = <<~'MESSAGE'
|
12
|
+
Killfork exited nonzero. Its result (if any) was ignored:
|
13
|
+
%s
|
14
|
+
MESSAGE
|
15
|
+
|
16
|
+
EXCEPTION_ERROR_MESSAGE = <<~'MESSAGE'
|
17
|
+
Killing the mutation resulted in an integration error.
|
18
|
+
This is the case when the tests selected for the current mutation
|
19
|
+
did not produce a test result, but instead an exception was raised.
|
20
|
+
|
21
|
+
This may point to the following problems:
|
22
|
+
* Bug in mutant
|
23
|
+
* Bug in the ruby interpreter
|
24
|
+
* Bug in your test suite
|
25
|
+
* Bug in your test suite under concurrency
|
26
|
+
|
27
|
+
The following exception was raised:
|
28
|
+
|
29
|
+
```
|
30
|
+
%s
|
31
|
+
%s
|
32
|
+
```
|
33
|
+
MESSAGE
|
34
|
+
|
35
|
+
FORK_ERROR_MESSAGE = <<~'MESSAGE'
|
36
|
+
Forking the child process to isolate the mutation in failed.
|
37
|
+
This meant that either the RubyVM or your OS was under too much
|
38
|
+
pressure to add another child process.
|
39
|
+
|
40
|
+
Possible solutions are:
|
41
|
+
* Reduce concurrency
|
42
|
+
* Reduce locks
|
43
|
+
MESSAGE
|
44
|
+
|
45
|
+
MAP = {
|
46
|
+
Isolation::Fork::ChildError => :visit_child_error,
|
47
|
+
Isolation::Fork::ForkError => :visit_fork_error,
|
48
|
+
Isolation::Result::ErrorChain => :visit_chain,
|
49
|
+
Isolation::Result::Exception => :visit_exception,
|
50
|
+
Isolation::Result::Success => :visit_success
|
51
|
+
}.freeze
|
52
|
+
|
53
|
+
private_constant(*constants(false))
|
54
|
+
|
55
|
+
# Run report printer
|
56
|
+
#
|
57
|
+
# @return [undefined]
|
58
|
+
def run
|
59
|
+
__send__(MAP.fetch(object.class))
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Visit successful isolation result
|
65
|
+
#
|
66
|
+
# @return [undefined]
|
67
|
+
def visit_success
|
68
|
+
visit(TestResult, object.value)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Visit child error isolation result
|
72
|
+
#
|
73
|
+
# @return [undefined]
|
74
|
+
def visit_child_error
|
75
|
+
puts(CHILD_ERROR_MESSAGE % object.value.inspect)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Visit fork error isolation result
|
79
|
+
#
|
80
|
+
# @return [undefined]
|
81
|
+
def visit_fork_error
|
82
|
+
puts(FORK_ERROR_MESSAGE)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Visit exception isolation result
|
86
|
+
#
|
87
|
+
# @return [undefined]
|
88
|
+
def visit_exception
|
89
|
+
exception = object.value
|
90
|
+
|
91
|
+
puts(
|
92
|
+
EXCEPTION_ERROR_MESSAGE % [
|
93
|
+
exception.inspect,
|
94
|
+
exception.backtrace.join("\n")
|
95
|
+
]
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Visit chain
|
100
|
+
#
|
101
|
+
# @return [undefined]
|
102
|
+
def visit_chain
|
103
|
+
printer = self.class
|
104
|
+
|
105
|
+
visit(printer, object.value)
|
106
|
+
visit(printer, object.next)
|
107
|
+
end
|
108
|
+
end # IsolationResult
|
109
|
+
end # Printer
|
110
|
+
end # CLI
|
111
|
+
end # Reporter
|
112
|
+
end # Mutant
|