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 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