mutant 0.10.0 → 0.10.7

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.
Files changed (41) 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 +78 -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.rb +1 -1
  14. data/lib/mutant/license/subscription/commercial.rb +2 -3
  15. data/lib/mutant/license/subscription/opensource.rb +2 -2
  16. data/lib/mutant/matcher/config.rb +13 -0
  17. data/lib/mutant/matcher/method/instance.rb +0 -2
  18. data/lib/mutant/mutator/node/send.rb +1 -1
  19. data/lib/mutant/parallel.rb +0 -1
  20. data/lib/mutant/parallel/worker.rb +0 -2
  21. data/lib/mutant/reporter/cli.rb +0 -2
  22. data/lib/mutant/reporter/cli/printer/config.rb +9 -5
  23. data/lib/mutant/reporter/cli/printer/coverage_result.rb +19 -0
  24. data/lib/mutant/reporter/cli/printer/env_progress.rb +2 -0
  25. data/lib/mutant/reporter/cli/printer/isolation_result.rb +19 -35
  26. data/lib/mutant/reporter/cli/printer/mutation_result.rb +4 -9
  27. data/lib/mutant/reporter/cli/printer/subject_result.rb +2 -2
  28. data/lib/mutant/result.rb +81 -30
  29. data/lib/mutant/runner/sink.rb +12 -5
  30. data/lib/mutant/selector/expression.rb +3 -1
  31. data/lib/mutant/test.rb +1 -1
  32. data/lib/mutant/timer.rb +60 -11
  33. data/lib/mutant/transform.rb +25 -21
  34. data/lib/mutant/version.rb +1 -1
  35. data/lib/mutant/warnings.rb +0 -1
  36. data/lib/mutant/world.rb +67 -0
  37. metadata +13 -15
  38. data/lib/mutant/minitest/coverage.rb +0 -53
  39. data/lib/mutant/reporter/cli/printer/mutation_progress_result.rb +0 -28
  40. data/lib/mutant/reporter/cli/printer/subject_progress.rb +0 -58
  41. 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
@@ -38,7 +38,7 @@ module Mutant
38
38
  # @return [String]
39
39
  def description
40
40
  FORMAT % {
41
- licensed: licensed.join("\n"),
41
+ licensed: licensed.to_a.join("\n"),
42
42
  subscription_name: subscription_name
43
43
  }
44
44
  end
@@ -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
@@ -46,6 +46,7 @@ module Mutant
46
46
  value
47
47
  .fetch('repositories')
48
48
  .map(&Repository.public_method(:parse))
49
+ .to_set
49
50
  )
50
51
  end
51
52
 
@@ -59,14 +60,13 @@ module Mutant
59
60
  private
60
61
 
61
62
  def check_subscription(actual)
62
- if (licensed.to_set & actual).any?
63
+ if (licensed & actual).any?
63
64
  success
64
65
  else
65
66
  failure(licensed, actual)
66
67
  end
67
68
  end
68
69
 
69
- # ignore :reek:UtilityFunction
70
70
  def parse_remotes(input)
71
71
  input.lines.map(&Repository.method(:parse_remote)).to_set
72
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 =