mutant 0.7.3 → 0.7.4

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.travis.yml +3 -2
  4. data/Changelog.md +7 -2
  5. data/Gemfile +0 -1
  6. data/Gemfile.devtools +9 -37
  7. data/README.md +1 -1
  8. data/circle.yml +1 -1
  9. data/config/flay.yml +1 -1
  10. data/config/reek.yml +6 -19
  11. data/config/rubocop.yml +58 -63
  12. data/lib/mutant.rb +8 -4
  13. data/lib/mutant/ast.rb +1 -1
  14. data/lib/mutant/cli.rb +12 -6
  15. data/lib/mutant/env.rb +17 -1
  16. data/lib/mutant/isolation.rb +4 -2
  17. data/lib/mutant/loader.rb +4 -0
  18. data/lib/mutant/matcher/compiler.rb +2 -0
  19. data/lib/mutant/mutation.rb +2 -0
  20. data/lib/mutant/mutator/node/const.rb +1 -1
  21. data/lib/mutant/mutator/node/generic.rb +1 -1
  22. data/lib/mutant/mutator/node/if.rb +2 -0
  23. data/lib/mutant/parallel.rb +93 -0
  24. data/lib/mutant/{runner → parallel}/master.rb +90 -45
  25. data/lib/mutant/parallel/source.rb +73 -0
  26. data/lib/mutant/{runner → parallel}/worker.rb +13 -30
  27. data/lib/mutant/reporter/cli.rb +8 -11
  28. data/lib/mutant/reporter/cli/printer.rb +14 -8
  29. data/lib/mutant/result.rb +0 -10
  30. data/lib/mutant/runner.rb +49 -43
  31. data/lib/mutant/runner/{scheduler.rb → sink.rb} +9 -68
  32. data/lib/mutant/version.rb +1 -1
  33. data/lib/mutant/zombifier/file.rb +28 -9
  34. data/meta/if.rb +8 -0
  35. data/meta/match_current_line.rb +1 -0
  36. data/spec/integration/mutant/corpus_spec.rb +1 -1
  37. data/spec/integration/mutant/null_spec.rb +1 -1
  38. data/spec/integration/mutant/rspec_spec.rb +1 -1
  39. data/spec/integration/mutant/test_mutator_handles_types_spec.rb +1 -1
  40. data/spec/integration/mutant/zombie_spec.rb +1 -1
  41. data/spec/support/corpus.rb +2 -0
  42. data/spec/support/fake_actor.rb +20 -10
  43. data/spec/support/mutation_verifier.rb +2 -0
  44. data/spec/support/shared_context.rb +12 -19
  45. data/spec/unit/mutant/env_spec.rb +20 -2
  46. data/spec/unit/mutant/expression_spec.rb +4 -1
  47. data/spec/unit/mutant/parallel/master_spec.rb +339 -0
  48. data/spec/unit/mutant/parallel/source/array_spec.rb +47 -0
  49. data/spec/unit/mutant/{runner → parallel}/worker_spec.rb +23 -26
  50. data/spec/unit/mutant/parallel_spec.rb +16 -0
  51. data/spec/unit/mutant/reporter/cli_spec.rb +1 -1
  52. data/spec/unit/mutant/reporter/trace_spec.rb +9 -0
  53. data/spec/unit/mutant/result/env_spec.rb +0 -55
  54. data/spec/unit/mutant/runner/driver_spec.rb +26 -0
  55. data/spec/unit/mutant/runner/sink_spec.rb +162 -0
  56. data/spec/unit/mutant/runner_spec.rb +60 -63
  57. data/spec/unit/mutant/warning_filter_spec.rb +2 -1
  58. data/test_app/Gemfile.devtools +9 -37
  59. metadata +21 -11
  60. data/spec/unit/mutant/runner/master_spec.rb +0 -199
  61. data/spec/unit/mutant/runner/scheduler_spec.rb +0 -161
@@ -71,6 +71,22 @@ module Mutant
71
71
  #
72
72
  attr_reader :matchable_scopes
73
73
 
74
+ # Kill mutation
75
+ #
76
+ # @param [Mutation] mutation
77
+ #
78
+ # @return [Result::Mutation]
79
+ #
80
+ # @api private
81
+ #
82
+ def kill_mutation(mutation)
83
+ test_result = mutation.kill(config.isolation, config.integration)
84
+ Result::Mutation.new(
85
+ mutation: mutation,
86
+ test_result: test_result
87
+ )
88
+ end
89
+
74
90
  private
75
91
 
76
92
  # Return scope name
@@ -109,7 +125,7 @@ module Mutant
109
125
  def expression(scope)
110
126
  name = scope_name(scope) or return
111
127
 
112
- unless name.is_a?(String)
128
+ unless name.instance_of?(String)
113
129
  warn("#{scope.class}#name from: #{scope.inspect} returned #{name.inspect}. #{SEMANTICS_MESSAGE}")
114
130
  return
115
131
  end
@@ -17,7 +17,7 @@ module Mutant
17
17
  def self.call(&block)
18
18
  block.call
19
19
  rescue => exception
20
- fail Error, exception
20
+ raise Error, exception
21
21
  end
22
22
  end
23
23
 
@@ -37,6 +37,8 @@ module Mutant
37
37
  #
38
38
  # @api private
39
39
  #
40
+ # rubocop:disable MethodLength
41
+ #
40
42
  def self.call(&block)
41
43
  reader, writer = IO.pipe.map(&:binmode)
42
44
 
@@ -52,7 +54,7 @@ module Mutant
52
54
  writer.close
53
55
  Marshal.load(reader.read)
54
56
  rescue => exception
55
- fail Error, exception
57
+ raise Error, exception
56
58
  ensure
57
59
  Process.waitpid(pid) if pid
58
60
  end
@@ -12,6 +12,10 @@ module Mutant
12
12
  #
13
13
  # @api private
14
14
  #
15
+ # One off the very few valid uses of eval
16
+ #
17
+ # rubocop:disable Lint/Eval
18
+ #
15
19
  def call
16
20
  eval(
17
21
  source,
@@ -42,6 +42,8 @@ module Mutant
42
42
  #
43
43
  # @api private
44
44
  #
45
+ # rubocop:disable MethodLength
46
+ #
45
47
  def predicate
46
48
  if subject_selector && subject_rejector
47
49
  Morpher::Evaluator::Predicate::Boolean::And.new([
@@ -16,6 +16,8 @@ module Mutant
16
16
  #
17
17
  # @api private
18
18
  #
19
+ # rubocop:disable MethodLength
20
+ #
19
21
  def kill(isolation, integration)
20
22
  start = Time.now
21
23
  tests = subject.tests
@@ -19,7 +19,7 @@ module Mutant
19
19
  emit_singletons unless parent_node && n_const?(parent_node)
20
20
  emit_type(nil, *children.drop(1))
21
21
  children.each_with_index do |child, index|
22
- mutate_child(index) if child.is_a?(Parser::AST::Node)
22
+ mutate_child(index) if child.instance_of?(Parser::AST::Node)
23
23
  end
24
24
  end
25
25
 
@@ -26,7 +26,7 @@ module Mutant
26
26
  #
27
27
  def dispatch
28
28
  children.each_with_index do |child, index|
29
- mutate_child(index) if child.is_a?(Parser::AST::Node)
29
+ mutate_child(index) if child.instance_of?(Parser::AST::Node)
30
30
  end
31
31
  end
32
32
 
@@ -47,6 +47,7 @@ module Mutant
47
47
  def mutate_if_branch
48
48
  emit_type(condition, else_branch, nil) if else_branch
49
49
  return unless if_branch
50
+ emit(if_branch)
50
51
  emit_if_branch_mutations
51
52
  emit_type(condition, if_branch, nil)
52
53
  end
@@ -59,6 +60,7 @@ module Mutant
59
60
  #
60
61
  def mutate_else_branch
61
62
  return unless else_branch
63
+ emit(else_branch)
62
64
  emit_else_branch_mutations
63
65
  emit_type(condition, nil, else_branch)
64
66
  end
@@ -0,0 +1,93 @@
1
+ module Mutant
2
+ # Parallel excecution engine of arbitrary payloads
3
+ module Parallel
4
+
5
+ # Driver for parallelized execution
6
+ class Driver
7
+ include Concord.new(:binding)
8
+
9
+ # Return scheduler status
10
+ #
11
+ # @return [Object]
12
+ #
13
+ # @api private
14
+ #
15
+ def status
16
+ binding.call(__method__)
17
+ end
18
+
19
+ # Stop master gracefully
20
+ #
21
+ # @return [self]
22
+ #
23
+ # @api private
24
+ #
25
+ def stop
26
+ binding.call(__method__)
27
+ self
28
+ end
29
+ end # Driver
30
+
31
+ # Run async computation returing driver
32
+ #
33
+ # @return [Driver]
34
+ #
35
+ # @api private
36
+ #
37
+ def self.async(config)
38
+ Driver.new(config.env.new_mailbox.bind(Master.call(config)))
39
+ end
40
+
41
+ # Job result sink
42
+ class Sink
43
+ include AbstractType
44
+
45
+ # Process job result
46
+ #
47
+ # @param [Object]
48
+ #
49
+ # @return [self]
50
+ #
51
+ # @api private
52
+ #
53
+ abstract_method :result
54
+
55
+ # Return status
56
+ #
57
+ # @return [Object]
58
+ #
59
+ # @api private
60
+ #
61
+ abstract_method :status
62
+
63
+ # Test if processing should stop
64
+ #
65
+ # @return [Boolean]
66
+ #
67
+ # @api private
68
+ #
69
+ abstract_method :stop?
70
+ end # Sink
71
+
72
+ # Job to push to workers
73
+ class Job
74
+ include Adamantium::Flat, Anima.new(:index, :payload)
75
+ end # Job
76
+
77
+ # Job result object received from workers
78
+ class JobResult
79
+ include Adamantium::Flat, Anima.new(:job, :payload)
80
+ end # JobResult
81
+
82
+ # Parallel run configuration
83
+ class Config
84
+ include Anima::Update, Adamantium::Flat, Anima.new(:env, :processor, :source, :sink, :jobs)
85
+ end # Config
86
+
87
+ # Parallel execution status
88
+ class Status
89
+ include Adamantium::Flat, Anima::Update, Anima.new(:payload, :done, :active_jobs)
90
+ end
91
+
92
+ end # Parallel
93
+ end # Mutant
@@ -1,8 +1,8 @@
1
1
  module Mutant
2
- class Runner
3
- # Master actor to control workers
2
+ module Parallel
3
+ # Master parallel worker
4
4
  class Master
5
- include Concord.new(:env, :actor)
5
+ include Concord.new(:config, :actor)
6
6
 
7
7
  private_class_method :new
8
8
 
@@ -14,14 +14,12 @@ module Mutant
14
14
  #
15
15
  # @api private
16
16
  #
17
- def self.call(env)
18
- env.config.actor_env.spawn do |actor|
19
- new(env, actor).__send__(:run)
17
+ def self.call(config)
18
+ config.env.spawn do |actor|
19
+ new(config, actor).__send__(:run)
20
20
  end
21
21
  end
22
22
 
23
- private
24
-
25
23
  # Initialize object
26
24
  #
27
25
  # @return [undefined]
@@ -31,30 +29,44 @@ module Mutant
31
29
  def initialize(*)
32
30
  super
33
31
 
34
- @scheduler = Scheduler.new(env)
35
- @workers = env.config.jobs
36
- @stop = false
37
- @stopping = false
32
+ @stop = false
33
+ @workers = 0
34
+ @active_jobs = Set.new
35
+ @index = 0
38
36
  end
39
37
 
38
+ private
39
+
40
40
  # Run work loop
41
41
  #
42
42
  # @return [self]
43
43
  #
44
44
  # @api private
45
45
  #
46
+ # rubocop:disable MethodLength
47
+ #
46
48
  def run
47
- @workers.times do |id|
48
- Worker.run(
49
- id: id,
50
- config: env.config,
51
- parent: actor.sender
52
- )
49
+ config.jobs.times do
50
+ @workers += 1
51
+ config.env.spawn do |worker_actor|
52
+ Worker.run(
53
+ actor: worker_actor,
54
+ processor: config.processor,
55
+ parent: actor.sender
56
+ )
57
+ end
53
58
  end
54
59
 
55
60
  receive_loop
56
61
  end
57
62
 
63
+ MAP = IceNine.deep_freeze(
64
+ ready: :handle_ready,
65
+ status: :handle_status,
66
+ result: :handle_result,
67
+ stop: :handle_stop
68
+ )
69
+
58
70
  # Handle messages
59
71
  #
60
72
  # @param [Actor::Message] message
@@ -65,18 +77,10 @@ module Mutant
65
77
  #
66
78
  def handle(message)
67
79
  type, payload = message.type, message.payload
68
- case type
69
- when :ready
70
- ready_worker(payload)
71
- when :status
72
- handle_status(payload)
73
- when :result
74
- handle_result(payload)
75
- when :stop
76
- handle_stop(payload)
77
- else
80
+ method = MAP.fetch(type) do
78
81
  fail Actor::ProtocolError, "Unexpected message: #{type.inspect}"
79
82
  end
83
+ __send__(method, payload)
80
84
  end
81
85
 
82
86
  # Run receive loop
@@ -86,10 +90,7 @@ module Mutant
86
90
  # @api private
87
91
  #
88
92
  def receive_loop
89
- loop do
90
- break if @workers.zero? && @stop
91
- handle(actor.receiver.call)
92
- end
93
+ handle(actor.receiver.call) until @workers.zero? && @stop
93
94
  end
94
95
 
95
96
  # Handle status
@@ -101,7 +102,12 @@ module Mutant
101
102
  # @api private
102
103
  #
103
104
  def handle_status(sender)
104
- sender.call(Actor::Message.new(:status, @scheduler.status))
105
+ status = Status.new(
106
+ payload: sink.status,
107
+ done: sink.stop? || @workers.zero?,
108
+ active_jobs: @active_jobs.dup.freeze
109
+ )
110
+ sender.call(Actor::Message.new(:status, status))
105
111
  end
106
112
 
107
113
  # Handle result
@@ -113,9 +119,8 @@ module Mutant
113
119
  # @api private
114
120
  #
115
121
  def handle_result(job_result)
116
- return if @stopping
117
- @scheduler.job_result(job_result)
118
- @stopping = env.config.fail_fast && @scheduler.status.done
122
+ @active_jobs.delete(job_result.job)
123
+ sink.result(job_result.payload)
119
124
  end
120
125
 
121
126
  # Handle stop
@@ -127,7 +132,6 @@ module Mutant
127
132
  # @api private
128
133
  #
129
134
  def handle_stop(sender)
130
- @stopping = true
131
135
  @stop = true
132
136
  receive_loop
133
137
  sender.call(Actor::Message.new(:stop))
@@ -141,18 +145,29 @@ module Mutant
141
145
  #
142
146
  # @api private
143
147
  #
144
- def ready_worker(sender)
145
- if @stopping
148
+ def handle_ready(sender)
149
+ if stop_work?
146
150
  stop_worker(sender)
147
151
  return
148
152
  end
149
153
 
150
- job = @scheduler.next_job
154
+ sender.call(Actor::Message.new(:job, next_job))
155
+ end
151
156
 
152
- if job
153
- sender.call(Actor::Message.new(:job, job))
154
- else
155
- stop_worker(sender)
157
+ # Return next job if any
158
+ #
159
+ # @return [Job]
160
+ # if next job is available
161
+ #
162
+ # @return [nil]
163
+ #
164
+ def next_job
165
+ Job.new(
166
+ index: @index,
167
+ payload: source.next
168
+ ).tap do |job|
169
+ @index += 1
170
+ @active_jobs << job
156
171
  end
157
172
  end
158
173
 
@@ -169,6 +184,36 @@ module Mutant
169
184
  sender.call(Actor::Message.new(:stop))
170
185
  end
171
186
 
187
+ # Test if scheduling stopped
188
+ #
189
+ # @return [Boolean]
190
+ #
191
+ # @api private
192
+ #
193
+ def stop_work?
194
+ @stop || !source.next? || sink.stop?
195
+ end
196
+
197
+ # Return source
198
+ #
199
+ # @return [Source]
200
+ #
201
+ # @api private
202
+ #
203
+ def source
204
+ config.source
205
+ end
206
+
207
+ # Return source
208
+ #
209
+ # @return [Sink]
210
+ #
211
+ # @api private
212
+ #
213
+ def sink
214
+ config.sink
215
+ end
216
+
172
217
  end # Master
173
- end # Runner
218
+ end # Parallel
174
219
  end # Mutant
@@ -0,0 +1,73 @@
1
+ module Mutant
2
+ module Parallel
3
+ # Job source for parallel execution
4
+ class Source
5
+ include AbstractType
6
+
7
+ NoJobError = Class.new(RuntimeError)
8
+
9
+ # Return next job
10
+ #
11
+ # @return [Object]
12
+ #
13
+ # @raise [NoJobError]
14
+ # when no next job is available
15
+ #
16
+ # @api private
17
+ #
18
+ abstract_method :next
19
+
20
+ # Test if next job is available
21
+ #
22
+ # @return [Boolean]
23
+ #
24
+ # @api private
25
+ #
26
+ abstract_method :next?
27
+
28
+ # Job source backed by a finite array
29
+ class Array
30
+ include Concord.new(:jobs)
31
+
32
+ # Initialize objecto
33
+ #
34
+ # @return [undefined]
35
+ #
36
+ # @api private
37
+ #
38
+ def initialize(*)
39
+ super
40
+
41
+ @next_index = 0
42
+ end
43
+
44
+ # Test if next job is available
45
+ #
46
+ # @return [Boolean]
47
+ #
48
+ # @api private
49
+ #
50
+ def next?
51
+ @next_index < jobs.length
52
+ end
53
+
54
+ # Return next job
55
+ #
56
+ # @return [Object]
57
+ #
58
+ # @raise [NoJobError]
59
+ # when no next job is available
60
+ #
61
+ # @api private
62
+ #
63
+ def next
64
+ fail NoJobError unless next?
65
+ jobs.fetch(@next_index).tap do
66
+ @next_index += 1
67
+ end
68
+ end
69
+
70
+ end # Array
71
+ end # Source
72
+ end # Parallel
73
+ end # Mutant