mutant 0.8.22 → 0.8.23
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/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
|