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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 544623c000b4f06105f72af9d4c33fcf08191ca2e25c330b96ddb876ec70adaa
4
- data.tar.gz: a2841e24767a138d0abc848528b5cf8fbf9c48b336b2aaf7854d3de72faa0aa1
3
+ metadata.gz: da1d34448bf320507db377b41c46f591022e69e02597f4097a9a8ff52cf7b7a8
4
+ data.tar.gz: fd4a4c3d03d89ec53e5e7a040c0f3e4854d58d2bd1347404f06cae2541db742c
5
5
  SHA512:
6
- metadata.gz: 476f0f3ba2af5bb6d900d196838935835c03f03bf13dc46c272fb71991496b4b97a98a648d7a55e327b040a1dfde1af0ddb6c2ff0e28cd315e56e4dc7e511d88
7
- data.tar.gz: 01ea1d5759f695df5d1fc52119d24d779d7ce686e04ad8f98d7618f435f77472bcd2b5482141a420238347826e8c7e29707ede272b7534a1b0ccd261833333f7
6
+ metadata.gz: e64e8ed9f28ee391b704aa37d18c4ec204ad3c73af56e3fed7d4430399bea663a599a0561d8e567b7d4fa9416b53ba810a5df95529c47345dfe18781a2367be0
7
+ data.tar.gz: be1ec3a98dfbf5b55dbc8543cf61a0dcf6a187232d0fd3d640ddc31533aaa1ed658a433f36bdc1158eb81df4774da13df737a8c59dd4e331369045957da7d226
@@ -1,3 +1,8 @@
1
+ # v0.8.23 2018-12-23
2
+
3
+ * Improved isolation error reporting
4
+ * Errors between isolation and tests do not kill mutations anymore.
5
+
1
6
  # v0.8.22 2018-12-04
2
7
 
3
8
  * Remove hard ruby version requirement. 2.5 is still the only officially supported version.
@@ -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/mutation_result'
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/subject_progress'
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'
@@ -24,13 +24,25 @@ module Mutant
24
24
  #
25
25
  # @return [Result::Mutation]
26
26
  def kill(mutation)
27
- test_result = run_mutation_tests(mutation)
27
+ start = Timer.now
28
+
28
29
  Result::Mutation.new(
29
- mutation: mutation,
30
- test_result: test_result
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::Test]
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(tests)
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
@@ -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 [Object]
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 Anima.new(:process, :stderr, :stdout, :io, :devnull, :marshal)
7
+ include(
8
+ Adamantium::Flat,
9
+ Anima.new(:devnull, :io, :marshal, :process, :stderr, :stdout)
10
+ )
12
11
 
13
- # Prevent mutation from `process.fork` to `fork` to call Kernel#fork
14
- undef_method :fork
12
+ ATTRIBUTES = (anima.attribute_names + %i[block reader writer]).freeze
15
13
 
16
- # Call block in isolation
17
- #
18
- # @return [Object]
19
- # returns block execution result
20
- #
21
- # @raise [Error]
22
- # if block terminates abnormal
23
- def call(&block)
24
- io.pipe(binmode: true) do |pipes|
25
- parent(*pipes, &block)
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
- # Handle parent process
32
- #
33
- # @param [IO] reader
34
- # @param [IO] writer
35
- #
36
- # @return [undefined]
37
- def parent(reader, writer, &block)
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
- writer.close
43
- marshal.load(reader)
44
- ensure
45
- process.waitpid(pid) if pid
46
- end
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
- # Handle child process
49
- #
50
- # @param [IO] reader
51
- # @param [IO] writer
52
- #
53
- # @return [undefined]
54
- def child(reader, writer, &block)
55
- reader.close
56
- writer.binmode
57
- writer.syswrite(marshal.dump(result(&block)))
58
- writer.close
59
- end
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
- # The block result computed under silencing
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 [Object]
64
- def result
65
- devnull.call do |null|
66
- stderr.reopen(null)
67
- stdout.reopen(null)
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 [Object]
13
+ # @return [Result]
16
14
  #
17
- # @raise [Error]
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
- raise Error, exception
19
+ Result::Exception.new(exception)
23
20
  end
24
21
 
25
22
  end # None
@@ -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, :node, :subject)
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
- Unparser.unparse(node),
22
+ source,
23
23
  binding,
24
24
  subject.source_path.to_s,
25
25
  subject.source_line
@@ -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
- node: root,
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