mutant 0.10.4 → 0.10.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mutant +0 -2
  3. data/lib/mutant.rb +7 -5
  4. data/lib/mutant/cli/command.rb +8 -6
  5. data/lib/mutant/cli/command/run.rb +23 -10
  6. data/lib/mutant/config.rb +80 -77
  7. data/lib/mutant/env.rb +14 -4
  8. data/lib/mutant/integration.rb +7 -10
  9. data/lib/mutant/integration/null.rb +0 -1
  10. data/lib/mutant/isolation.rb +11 -48
  11. data/lib/mutant/isolation/fork.rb +107 -40
  12. data/lib/mutant/isolation/none.rb +18 -5
  13. data/lib/mutant/license/subscription/commercial.rb +2 -3
  14. data/lib/mutant/license/subscription/opensource.rb +0 -1
  15. data/lib/mutant/matcher/config.rb +13 -0
  16. data/lib/mutant/matcher/method/instance.rb +0 -2
  17. data/lib/mutant/mutator/node/send.rb +1 -1
  18. data/lib/mutant/parallel.rb +0 -1
  19. data/lib/mutant/parallel/worker.rb +0 -2
  20. data/lib/mutant/reporter/cli.rb +0 -2
  21. data/lib/mutant/reporter/cli/printer/config.rb +9 -5
  22. data/lib/mutant/reporter/cli/printer/coverage_result.rb +19 -0
  23. data/lib/mutant/reporter/cli/printer/env_progress.rb +2 -0
  24. data/lib/mutant/reporter/cli/printer/isolation_result.rb +19 -35
  25. data/lib/mutant/reporter/cli/printer/mutation_result.rb +4 -9
  26. data/lib/mutant/reporter/cli/printer/subject_result.rb +2 -2
  27. data/lib/mutant/result.rb +91 -30
  28. data/lib/mutant/runner/sink.rb +12 -5
  29. data/lib/mutant/timer.rb +60 -11
  30. data/lib/mutant/transform.rb +25 -21
  31. data/lib/mutant/version.rb +1 -1
  32. data/lib/mutant/warnings.rb +0 -1
  33. data/lib/mutant/world.rb +67 -0
  34. metadata +12 -13
  35. data/lib/mutant/reporter/cli/printer/mutation_progress_result.rb +0 -28
  36. data/lib/mutant/reporter/cli/printer/subject_progress.rb +0 -58
  37. data/lib/mutant/reporter/cli/printer/test_result.rb +0 -32
@@ -4,7 +4,7 @@ module Mutant
4
4
 
5
5
  # Abstract base class mutant test framework integrations
6
6
  class Integration
7
- include AbstractType, Adamantium::Flat, Concord.new(:config)
7
+ include AbstractType, Adamantium::Flat, Anima.new(:expression_parser, :timer)
8
8
 
9
9
  LOAD_MESSAGE = <<~'MESSAGE'
10
10
  Unable to load integration mutant-%<integration_name>s:
@@ -27,9 +27,12 @@ module Mutant
27
27
  #
28
28
  # @return [Either<String, Integration>]
29
29
  def self.setup(env)
30
- attempt_require(env)
31
- .bind { attempt_const_get(env) }
32
- .fmap { |klass| klass.new(env.config).setup }
30
+ attempt_require(env).bind { attempt_const_get(env) }.fmap do |klass|
31
+ klass.new(
32
+ expression_parser: env.config.expression_parser,
33
+ timer: env.world.timer
34
+ ).setup
35
+ end
33
36
  end
34
37
 
35
38
  # rubocop:disable Style/MultilineBlockChain
@@ -80,11 +83,5 @@ module Mutant
80
83
  #
81
84
  # @return [Enumerable<Test>]
82
85
  abstract_method :all_tests
83
-
84
- private
85
-
86
- def expression_parser
87
- config.expression_parser
88
- end
89
86
  end # Integration
90
87
  end # Mutant
@@ -18,7 +18,6 @@ module Mutant
18
18
  # @return [Result::Test]
19
19
  def call(tests)
20
20
  Result::Test.new(
21
- output: '',
22
21
  passed: true,
23
22
  runtime: 0.0,
24
23
  tests: tests
@@ -7,57 +7,20 @@ module Mutant
7
7
 
8
8
  # Isolated computation result
9
9
  class Result
10
- include AbstractType, Adamantium
11
-
12
- NULL_LOG = ''
13
-
14
- private_constant(*constants(false))
15
-
16
- abstract_method :error
17
- abstract_method :next
18
- abstract_method :value
19
-
20
- # Add error on top of current result
21
- #
22
- # @param [Result] error
23
- #
24
- # @return [Result]
25
- def add_error(error)
26
- ErrorChain.new(error, self)
27
- end
28
-
29
- # The log captured from integration
30
- #
31
- # @return [String]
32
- def log
33
- NULL_LOG
34
- end
35
-
36
- # Test for success
10
+ include Anima.new(
11
+ :exception,
12
+ :log,
13
+ :process_status,
14
+ :timeout,
15
+ :value
16
+ )
17
+
18
+ # Test for successful result
37
19
  #
38
20
  # @return [Boolean]
39
- def success?
40
- instance_of?(Success)
21
+ def valid_value?
22
+ timeout.nil? && exception.nil? && (process_status.nil? || process_status.success?)
41
23
  end
42
-
43
- # Successful result producing value
44
- class Success < self
45
- include Concord::Public.new(:value, :log)
46
-
47
- def self.new(_value, _log = '')
48
- super
49
- end
50
- end # Success
51
-
52
- # Unsuccessful result by unexpected exception
53
- class Exception < self
54
- include Concord::Public.new(:value)
55
- end # Error
56
-
57
- # Result when there where many results
58
- class ErrorChain < Result
59
- include Concord::Public.new(:value, :next)
60
- end # ChainError
61
24
  end # Result
62
25
 
63
26
  # Call block in isolation
@@ -3,22 +3,36 @@
3
3
  module Mutant
4
4
  class Isolation
5
5
  # Isolation via the fork(2) systemcall.
6
+ #
7
+ # Communication between parent and child process is done
8
+ # via anonymous pipes.
9
+ #
10
+ # Timeouts are initially handled relatively efficiently via IO.select
11
+ # but once the child process pipes are on eof via busy looping on
12
+ # waitpid2 with Process::WNOHANG set.
13
+ #
14
+ # Handling timeouts this way is not the conceptually most
15
+ # efficient solution. But its cross platform.
16
+ #
17
+ # Design constraints:
18
+ #
19
+ # * Support Linux
20
+ # * Support MacOSX
21
+ # * Avoid platform specific APIs and code.
22
+ # * Only use ruby corelib.
23
+ # * Do not use any named resource.
24
+ # * Never block on latency inducing systemcall without a
25
+ # timeout.
26
+ # * Child process freezing before closing the pipes needs to
27
+ # be detected by parent process.
28
+ # * Child process freezing after closing the pipes needs to be
29
+ # detected by parent process.
6
30
  class Fork < self
7
31
  include(Adamantium::Flat, Concord.new(:world))
8
32
 
9
33
  READ_SIZE = 4096
10
34
 
11
- ATTRIBUTES = %i[block log_pipe result_pipe world].freeze
12
-
13
- # Unsuccessful result as child exited nonzero
14
- class ChildError < Result
15
- include Concord::Public.new(:value, :log)
16
- end # ChildError
17
-
18
- # Unsuccessful result as fork failed
19
- class ForkError < Result
20
- include Equalizer.new
21
- end # ForkError
35
+ ATTRIBUTES = %i[block deadline log_pipe result_pipe world].freeze
22
36
 
23
37
  # Pipe abstraction
24
38
  class Pipe
@@ -50,7 +64,6 @@ module Mutant
50
64
  end
51
65
  end # Pipe
52
66
 
53
- # ignore :reek:InstanceVariableAssumption
54
67
  class Parent
55
68
  include(
56
69
  Anima.new(*ATTRIBUTES),
@@ -64,15 +77,28 @@ module Mutant
64
77
  #
65
78
  # @return [Result]
66
79
  def call
67
- pid = start_child or return ForkError.new
68
-
69
- read_child_result(pid)
70
-
71
- @result
80
+ @exception = nil
81
+ @log_fragments = []
82
+ @timeout = nil
83
+ @value = nil
84
+ @pid = start_child
85
+
86
+ read_child_result
87
+ result
72
88
  end
73
89
 
74
90
  private
75
91
 
92
+ def result
93
+ Result.new(
94
+ exception: @exception,
95
+ log: @log_fragments.join,
96
+ process_status: @process_status,
97
+ timeout: @timeout,
98
+ value: @value
99
+ )
100
+ end
101
+
76
102
  def start_child
77
103
  world.process.fork do
78
104
  Child.call(
@@ -85,30 +111,43 @@ module Mutant
85
111
  end
86
112
 
87
113
  # rubocop:disable Metrics/MethodLength
88
- def read_child_result(pid)
114
+ def read_child_result
89
115
  result_fragments = []
90
- log_fragments = []
91
116
 
92
- read_fragments(
93
- log_pipe.parent => log_fragments,
94
- result_pipe.parent => result_fragments
95
- )
117
+ targets =
118
+ {
119
+ log_pipe.parent => @log_fragments,
120
+ result_pipe.parent => result_fragments
121
+ }
96
122
 
97
- begin
98
- result = world.marshal.load(result_fragments.join)
99
- rescue ArgumentError => exception
100
- add_result(Result::Exception.new(exception))
123
+ read_targets(targets)
124
+
125
+ if targets.empty?
126
+ load_result(result_fragments)
127
+ terminate_graceful
101
128
  else
102
- add_result(Result::Success.new(result, log_fragments.join))
129
+ @timeout = deadline.allowed_time
130
+ terminate_ungraceful
103
131
  end
104
- ensure
105
- wait_child(pid, log_fragments)
106
132
  end
107
133
  # rubocop:enable Metrics/MethodLength
108
134
 
109
- def read_fragments(targets)
135
+ def load_result(result_fragments)
136
+ @value = world.marshal.load(result_fragments.join)
137
+ rescue ArgumentError => exception
138
+ @exception = exception
139
+ end
140
+
141
+ # rubocop:disable Metrics/MethodLength
142
+ def read_targets(targets)
110
143
  until targets.empty?
111
- ready, = world.io.select(targets.keys)
144
+ status = deadline.status
145
+
146
+ break unless status.ok?
147
+
148
+ ready, = world.io.select(targets.keys, [], [], status.time_left)
149
+
150
+ break unless ready
112
151
 
113
152
  ready.each do |fd|
114
153
  if fd.eof?
@@ -119,14 +158,42 @@ module Mutant
119
158
  end
120
159
  end
121
160
  end
161
+ # rubocop:enable Metrics/MethodLength
122
162
 
123
- def wait_child(pid, log_fragments)
124
- _pid, status = world.process.wait2(pid)
163
+ # rubocop:disable Metrics/MethodLength
164
+ def terminate_graceful
165
+ status = nil
166
+
167
+ loop do
168
+ status = peek_child
169
+ break if status || deadline.expired?
170
+ world.kernel.sleep(0.1)
171
+ end
125
172
 
126
- unless status.success? # rubocop:disable Style/GuardClause
127
- add_result(ChildError.new(status, log_fragments.join))
173
+ if status
174
+ handle_status(status)
175
+ else
176
+ terminate_ungraceful
128
177
  end
129
178
  end
179
+ # rubocop:enable Metrics/MethodLength
180
+
181
+ def terminate_ungraceful
182
+ world.process.kill('KILL', @pid)
183
+
184
+ _pid, status = world.process.wait2(@pid)
185
+
186
+ handle_status(status)
187
+ end
188
+
189
+ def handle_status(status)
190
+ @process_status = status
191
+ end
192
+
193
+ def peek_child
194
+ _pid, status = world.process.wait2(@pid, Process::WNOHANG)
195
+ status
196
+ end
130
197
 
131
198
  def add_result(result)
132
199
  @result = defined?(@result) ? @result.add_error(result) : result
@@ -157,17 +224,16 @@ module Mutant
157
224
  # Call block in isolation
158
225
  #
159
226
  # @return [Result]
160
- # execution result
161
- #
162
- # ignore :reek:NestedIterators
163
227
  #
164
228
  # rubocop:disable Metrics/MethodLength
165
- def call(&block)
229
+ def call(timeout, &block)
230
+ deadline = world.deadline(timeout)
166
231
  io = world.io
167
232
  Pipe.with(io) do |result|
168
233
  Pipe.with(io) do |log|
169
234
  Parent.call(
170
235
  block: block,
236
+ deadline: deadline,
171
237
  log_pipe: log,
172
238
  result_pipe: result,
173
239
  world: world
@@ -176,6 +242,7 @@ module Mutant
176
242
  end
177
243
  end
178
244
  # rubocop:enable Metrics/MethodLength
245
+
179
246
  end # Fork
180
247
  end # Isolation
181
248
  end # Mutant
@@ -12,12 +12,25 @@ module Mutant
12
12
  #
13
13
  # @return [Result]
14
14
  #
15
- # ignore :reek:UtilityFunction
16
- def call
17
- Result::Success.new(yield)
18
- rescue => exception
19
- Result::Exception.new(exception)
15
+ # rubocop:disable Lint/SuppressedException
16
+ # rubocop:disable Metrics/MethodLength
17
+ # ^^ it actually isn not suppressed, it assigns an lvar
18
+ def call(_timeout)
19
+ begin
20
+ value = yield
21
+ rescue => exception
22
+ end
23
+
24
+ Result.new(
25
+ exception: exception,
26
+ log: '',
27
+ process_status: nil,
28
+ timeout: nil,
29
+ value: value
30
+ )
20
31
  end
32
+ # rubocop:enable Lint/SuppressedException
33
+ # rubocop:enable Metrics/MethodLength
21
34
 
22
35
  end # None
23
36
  end # Isolation
@@ -12,7 +12,7 @@ module Mutant
12
12
  end
13
13
 
14
14
  def self.from_json(value)
15
- new(value.fetch('authors').map(&Author.method(:new)).to_set)
15
+ new(value.fetch('authors').map(&Author.public_method(:new)).to_set)
16
16
  end
17
17
 
18
18
  def apply(world)
@@ -39,12 +39,11 @@ module Mutant
39
39
  capture(world, %w[git show --quiet --pretty=format:%ae])
40
40
  end
41
41
 
42
- # ignore :reek:UtilityFunction
43
42
  def capture(world, command)
44
43
  world
45
44
  .capture_stdout(command)
46
45
  .fmap(&:chomp)
47
- .fmap(&Author.method(:new))
46
+ .fmap(&Author.public_method(:new))
48
47
  .fmap { |value| Set.new([value]) }
49
48
  .from_right { Set.new }
50
49
  end
@@ -67,7 +67,6 @@ module Mutant
67
67
  end
68
68
  end
69
69
 
70
- # ignore :reek:UtilityFunction
71
70
  def parse_remotes(input)
72
71
  input.lines.map(&Repository.method(:parse_remote)).to_set
73
72
  end
@@ -44,6 +44,19 @@ module Mutant
44
44
  with(attribute => public_send(attribute) + [value])
45
45
  end
46
46
 
47
+ # Merge with other config
48
+ #
49
+ # @param [Config] other
50
+ #
51
+ # @return [Config]
52
+ def merge(other)
53
+ self.class.new(
54
+ to_h
55
+ .map { |name, value| [name, value + other.public_send(name)] }
56
+ .to_h
57
+ )
58
+ end
59
+
47
60
  private
48
61
 
49
62
  def present_attributes
@@ -12,8 +12,6 @@ module Mutant
12
12
  # @param [UnboundMethod] method
13
13
  #
14
14
  # @return [Matcher::Method::Instance]
15
- #
16
- # :reek:ManualDispatch
17
15
  def self.new(scope, target_method)
18
16
  name = target_method.name
19
17
  evaluator =
@@ -99,7 +99,7 @@ module Mutant
99
99
 
100
100
  dynamic_selector, *actual_arguments = *arguments
101
101
 
102
- return unless n_sym?(dynamic_selector)
102
+ return unless dynamic_selector && n_sym?(dynamic_selector)
103
103
 
104
104
  method_name = AST::Meta::Symbol.new(dynamic_selector).name
105
105
 
@@ -41,7 +41,6 @@ module Mutant
41
41
  end
42
42
  private_class_method :threads
43
43
 
44
- # ignore :reek:LongParameterList
45
44
  def self.shared(klass, config, **attributes)
46
45
  klass.new(
47
46
  condition_variable: config.condition_variable,