mutant 0.6.7 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +10 -0
  3. data/README.md +1 -1
  4. data/config/flay.yml +1 -1
  5. data/config/reek.yml +11 -40
  6. data/config/rubocop.yml +1 -1
  7. data/lib/mutant.rb +10 -2
  8. data/lib/mutant/actor.rb +64 -0
  9. data/lib/mutant/actor/actor.rb +50 -0
  10. data/lib/mutant/actor/env.rb +35 -0
  11. data/lib/mutant/actor/mailbox.rb +53 -0
  12. data/lib/mutant/actor/receiver.rb +48 -0
  13. data/lib/mutant/actor/sender.rb +27 -0
  14. data/lib/mutant/ast/types.rb +1 -1
  15. data/lib/mutant/cli.rb +2 -0
  16. data/lib/mutant/config.rb +2 -1
  17. data/lib/mutant/env.rb +6 -2
  18. data/lib/mutant/expression/methods.rb +1 -1
  19. data/lib/mutant/integration.rb +11 -1
  20. data/lib/mutant/isolation.rb +1 -2
  21. data/lib/mutant/meta/example.rb +1 -1
  22. data/lib/mutant/mutation.rb +47 -21
  23. data/lib/mutant/mutator/node.rb +1 -1
  24. data/lib/mutant/mutator/node/literal/symbol.rb +1 -1
  25. data/lib/mutant/mutator/node/send.rb +4 -3
  26. data/lib/mutant/reporter/cli.rb +2 -0
  27. data/lib/mutant/reporter/cli/format.rb +23 -36
  28. data/lib/mutant/reporter/cli/printer.rb +66 -27
  29. data/lib/mutant/result.rb +45 -58
  30. data/lib/mutant/runner.rb +47 -154
  31. data/lib/mutant/runner/master.rb +174 -0
  32. data/lib/mutant/runner/scheduler.rb +141 -0
  33. data/lib/mutant/runner/worker.rb +93 -0
  34. data/lib/mutant/subject/method/instance.rb +1 -15
  35. data/lib/mutant/test.rb +2 -15
  36. data/lib/mutant/version.rb +1 -1
  37. data/meta/send.rb +16 -0
  38. data/mutant-rspec.gemspec +1 -1
  39. data/mutant.gemspec +1 -1
  40. data/spec/integration/mutant/rspec_spec.rb +0 -6
  41. data/spec/spec_helper.rb +9 -1
  42. data/spec/support/fake_actor.rb +93 -0
  43. data/spec/support/shared_context.rb +135 -0
  44. data/spec/unit/mutant/actor/actor_spec.rb +35 -0
  45. data/spec/unit/mutant/actor/binding_spec.rb +32 -0
  46. data/spec/unit/mutant/actor/env_spec.rb +49 -0
  47. data/spec/unit/mutant/actor/message_spec.rb +23 -0
  48. data/spec/unit/mutant/actor/receiver_spec.rb +60 -0
  49. data/spec/unit/mutant/actor/sender_spec.rb +22 -0
  50. data/spec/unit/mutant/cli_spec.rb +17 -4
  51. data/spec/unit/mutant/env_spec.rb +2 -2
  52. data/spec/unit/mutant/mailbox_spec.rb +33 -0
  53. data/spec/unit/mutant/mutation_spec.rb +52 -18
  54. data/spec/unit/mutant/mutator/registry_spec.rb +4 -4
  55. data/spec/unit/mutant/reporter/cli_spec.rb +131 -249
  56. data/spec/unit/mutant/result/env_spec.rb +55 -0
  57. data/spec/unit/mutant/result/subject_spec.rb +43 -0
  58. data/spec/unit/mutant/runner/master_spec.rb +199 -0
  59. data/spec/unit/mutant/runner/scheduler_spec.rb +161 -0
  60. data/spec/unit/mutant/runner/worker_spec.rb +73 -0
  61. data/spec/unit/mutant/runner_spec.rb +60 -118
  62. data/spec/unit/mutant/subject/method/instance_spec.rb +18 -31
  63. data/spec/unit/mutant/warning_filter_spec.rb +1 -1
  64. metadata +39 -14
  65. data/lib/mutant/runner/collector.rb +0 -133
  66. data/lib/mutant/warning_expectation.rb +0 -47
  67. data/spec/unit/mutant/runner/collector_spec.rb +0 -198
  68. data/spec/unit/mutant/test_spec.rb +0 -23
  69. data/spec/unit/mutant/warning_expectation_spec.rb +0 -80
  70. data/test_app/Gemfile.rspec2 +0 -6
@@ -0,0 +1,27 @@
1
+ module Mutant
2
+ module Actor
3
+
4
+ # Sender for messages to acting thread
5
+ class Sender
6
+ include Concord.new(:thread, :mutex, :mailbox)
7
+
8
+ # Send a message to actor
9
+ #
10
+ # @param [Object] message
11
+ #
12
+ # @return [self]
13
+ #
14
+ # @api private
15
+ #
16
+ def call(message)
17
+ mutex.synchronize do
18
+ mailbox << message
19
+ thread.run
20
+ end
21
+
22
+ self
23
+ end
24
+
25
+ end # Sender
26
+ end # Actor
27
+ end # Mutant
@@ -18,7 +18,7 @@ module Mutant
18
18
  INDEX_OPERATORS = symbolset.(%w[[] []=])
19
19
  UNARY_METHOD_OPERATORS = symbolset.(%w[~@ +@ -@ !])
20
20
 
21
- # Operators ruby implementeds as methods
21
+ # Operators ruby implements as methods
22
22
  METHOD_OPERATORS = symbolset.(%w[
23
23
  <=> === []= [] <= >= == !~ != =~ <<
24
24
  >> ** * % / | ^ & < > + - ~@ +@ -@ !
data/lib/mutant/cli.rb CHANGED
@@ -128,6 +128,8 @@ module Mutant
128
128
  def setup_integration(name)
129
129
  require "mutant/integration/#{name}"
130
130
  update(integration: Integration.lookup(name))
131
+ rescue LoadError
132
+ fail Error, "Could not load integration #{name.inspect} (you may want to try installing the gem mutant-#{name})"
131
133
  end
132
134
 
133
135
  # Add options
data/lib/mutant/config.rb CHANGED
@@ -12,7 +12,8 @@ module Mutant
12
12
  :fail_fast,
13
13
  :jobs,
14
14
  :zombie,
15
- :expected_coverage
15
+ :expected_coverage,
16
+ :actor_env
16
17
  )
17
18
 
18
19
  [:fail_fast, :zombie, :debug].each do |name|
data/lib/mutant/env.rb CHANGED
@@ -3,6 +3,10 @@ module Mutant
3
3
  class Env
4
4
  include Adamantium::Flat, Concord::Public.new(:config, :cache)
5
5
 
6
+ SEMANTICS_MESSAGE =
7
+ "Fix your lib to follow normal ruby semantics!\n" \
8
+ '{Module,Class}#name should return resolvable constant name as String or nil'.freeze
9
+
6
10
  # Return new env
7
11
  #
8
12
  # @param [Config] config
@@ -86,7 +90,7 @@ module Mutant
86
90
  def scope_name(scope)
87
91
  scope.name
88
92
  rescue => exception
89
- warn("#{scope.class}#name from: #{scope.inspect} raised an error: #{exception.inspect} fix your lib to follow normal ruby semantics!")
93
+ warn("#{scope.class}#name from: #{scope.inspect} raised an error: #{exception.inspect}. #{SEMANTICS_MESSAGE}")
90
94
  nil
91
95
  end
92
96
 
@@ -106,7 +110,7 @@ module Mutant
106
110
  name = scope_name(scope) or return
107
111
 
108
112
  unless name.is_a?(String)
109
- warn("#{scope.class}#name from: #{scope.inspect} returned #{name.inspect} instead String or nil. Fix your lib to follow normal ruby semantics!")
113
+ warn("#{scope.class}#name from: #{scope.inspect} returned #{name.inspect}. #{SEMANTICS_MESSAGE}")
110
114
  return
111
115
  end
112
116
 
@@ -1,7 +1,7 @@
1
1
  module Mutant
2
2
  class Expression
3
3
 
4
- # Abstrat base class for methods expression
4
+ # Abstract base class for methods expression
5
5
  class Methods < self
6
6
 
7
7
  MATCHERS = IceNine.deep_freeze(
@@ -16,7 +16,7 @@ module Mutant
16
16
  # @api private
17
17
  #
18
18
  def self.lookup(name)
19
- REGISTRY.fetch(name).build
19
+ REGISTRY.fetch(name).new
20
20
  end
21
21
 
22
22
  # Register integration
@@ -44,6 +44,16 @@ module Mutant
44
44
  self
45
45
  end
46
46
 
47
+ # Return test result for tests
48
+ #
49
+ # @param [Enumerable<Test>] tests
50
+ #
51
+ # @return [Result::Test]
52
+ #
53
+ # @api private
54
+ #
55
+ abstract_method :call
56
+
47
57
  # Return all available tests by integration
48
58
  #
49
59
  # @return [Enumerable<Test>]
@@ -50,8 +50,7 @@ module Mutant
50
50
  end
51
51
 
52
52
  writer.close
53
- result = Marshal.load(reader.read)
54
- result
53
+ Marshal.load(reader.read)
55
54
  rescue => exception
56
55
  fail Error, exception
57
56
  ensure
@@ -66,7 +66,7 @@ module Mutant
66
66
 
67
67
  private
68
68
 
69
- # Return unexpected mutationso
69
+ # Return unexpected mutations
70
70
  #
71
71
  # @return [Array<Parser::AST::Node>]
72
72
  #
@@ -7,21 +7,30 @@ module Mutant
7
7
  CODE_DELIMITER = "\0".freeze
8
8
  CODE_RANGE = (0..4).freeze
9
9
 
10
- # Insert mutated node
10
+ # Kill mutation under isolation with integration
11
11
  #
12
- # FIXME: Cache subject visibility in a better way! Ideally dont mutate it
13
- # implicitly. Also subject.public? should NOT be a public interface it
14
- # is a detail of method mutations.
12
+ # @param [Isolation] isolation
13
+ # @param [Integration] integration
15
14
  #
16
- # @return [self]
15
+ # @return [Result::Test]
17
16
  #
18
17
  # @api private
19
18
  #
20
- def insert
21
- subject.public?
22
- subject.prepare
23
- Loader::Eval.call(root, subject)
24
- self
19
+ def kill(isolation, integration)
20
+ start = Time.now
21
+ tests = subject.tests
22
+
23
+ isolation.call do
24
+ insert
25
+ integration.call(tests)
26
+ end.update(tests: tests)
27
+ rescue Isolation::Error => error
28
+ Result::Test.new(
29
+ tests: tests,
30
+ output: error.message,
31
+ runtime: Time.now - start,
32
+ passed: false
33
+ )
25
34
  end
26
35
 
27
36
  # Return identification
@@ -67,20 +76,37 @@ module Mutant
67
76
  subject.source
68
77
  end
69
78
 
70
- # Test if mutation is killed by test report
79
+ # Test if mutation is killed by test reports
71
80
  #
72
- # @param [Report::Test] test_report
81
+ # @param [Array<Report::Test>] test_reports
73
82
  #
74
83
  # @return [Boolean]
75
84
  #
76
85
  # @api private
77
86
  #
78
- def killed_by?(test_report)
79
- self.class::SHOULD_PASS.equal?(test_report.passed)
87
+ def self.success?(test_result)
88
+ self::TEST_PASS_SUCCESS.equal?(test_result.passed)
80
89
  end
81
90
 
82
91
  private
83
92
 
93
+ # Insert mutated node
94
+ #
95
+ # FIXME: Cache subject visibility in a better way! Ideally dont mutate it
96
+ # implicitly. Also subject.public? should NOT be a public interface it
97
+ # is a detail of method mutations.
98
+ #
99
+ # @return [self]
100
+ #
101
+ # @api private
102
+ #
103
+ def insert
104
+ subject.public?
105
+ subject.prepare
106
+ Loader::Eval.call(root, subject)
107
+ self
108
+ end
109
+
84
110
  # Return sha1 sum of source and subject identification
85
111
  #
86
112
  # @return [String]
@@ -105,24 +131,24 @@ module Mutant
105
131
  # Evil mutation that should case mutations to fail tests
106
132
  class Evil < self
107
133
 
108
- SHOULD_PASS = false
109
- SYMBOL = 'evil'.freeze
134
+ SYMBOL = 'evil'.freeze
135
+ TEST_PASS_SUCCESS = false
110
136
 
111
137
  end # Evil
112
138
 
113
139
  # Neutral mutation that should not cause mutations to fail tests
114
140
  class Neutral < self
115
141
 
116
- SYMBOL = 'neutral'.freeze
117
- SHOULD_PASS = true
142
+ SYMBOL = 'neutral'.freeze
143
+ TEST_PASS_SUCCESS = true
118
144
 
119
145
  end # Neutral
120
146
 
121
147
  # Noop mutation, special case of neutral
122
- class Noop < self
148
+ class Noop < Neutral
123
149
 
124
- SYMBOL = 'noop'.freeze
125
- SHOULD_PASS = true
150
+ SYMBOL = 'noop'.freeze
151
+ TEST_PASS_SUCCESS = true
126
152
 
127
153
  end # Noop
128
154
 
@@ -219,7 +219,7 @@ module Mutant
219
219
  #
220
220
  # @return [Enumerable<Fixnum>]
221
221
  #
222
- # @api pirvate
222
+ # @api private
223
223
  #
224
224
  def children_indices(range)
225
225
  range_end = range.end
@@ -13,7 +13,7 @@ module Mutant
13
13
 
14
14
  private
15
15
 
16
- # Emit mutatns
16
+ # Emit mutants
17
17
  #
18
18
  # @return [undefined]
19
19
  #
@@ -17,7 +17,8 @@ module Mutant
17
17
  reverse_each: [:each],
18
18
  reverse_merge: [:merge],
19
19
  map: [:each],
20
- send: [:public_send],
20
+ send: [:public_send, :__send__],
21
+ __send__: [:public_send],
21
22
  gsub: [:sub],
22
23
  eql?: [:equal?],
23
24
  to_s: [:to_str],
@@ -26,8 +27,8 @@ module Mutant
26
27
  :== => [:eql?, :equal?],
27
28
  :>= => [:>, :==, :eql?, :equal?],
28
29
  :<= => [:<, :==, :eql?, :equal?],
29
- :> => [:==, :eql?, :equal?],
30
- :< => [:==, :eql?, :equal?]
30
+ :> => [:==, :>=, :eql?, :equal?],
31
+ :< => [:==, :<=, :eql?, :equal?]
31
32
  )
32
33
 
33
34
  private
@@ -28,6 +28,8 @@ module Mutant
28
28
  #
29
29
  # @param [Env] env
30
30
  #
31
+ # @return [self]
32
+ #
31
33
  # @api private
32
34
  #
33
35
  def start(env)
@@ -17,7 +17,7 @@ module Mutant
17
17
 
18
18
  # Return progress representation
19
19
  #
20
- # @param [Runner::Collector] collector
20
+ # @param [Runner::Status] status
21
21
  #
22
22
  # @return [String]
23
23
  #
@@ -67,6 +67,18 @@ module Mutant
67
67
  # Format for progressive non rewindable output
68
68
  class Progressive < self
69
69
 
70
+ # Initialize object
71
+ #
72
+ # @return [undefined]
73
+ #
74
+ # @api private
75
+ #
76
+ def initialize(*)
77
+ @seen = Set.new
78
+
79
+ super
80
+ end
81
+
70
82
  # Return start representation
71
83
  #
72
84
  # @return [String]
@@ -83,10 +95,13 @@ module Mutant
83
95
  #
84
96
  # @api private
85
97
  #
86
- def progress(collector)
87
- last_mutation_result = collector.last_mutation_result
88
- return EMPTY_STRING unless last_mutation_result
89
- format(Printer::MutationProgressResult, last_mutation_result)
98
+ def progress(status)
99
+ current = status.env_result.subject_results.flat_map(&:mutation_results)
100
+ new = current.reject(&@seen.method(:include?))
101
+ @seen = current.to_set
102
+ new.map do |mutation_result|
103
+ format(Printer::MutationProgressResult, mutation_result)
104
+ end.join(EMPTY_STRING)
90
105
  end
91
106
 
92
107
  private
@@ -109,20 +124,6 @@ module Mutant
109
124
 
110
125
  BUFFER_FLAGS = 'a+'.freeze
111
126
 
112
- # Rate per second progress report fires
113
- OUTPUT_RATE = 1.0 / 20
114
-
115
- # Initialize object
116
- #
117
- # @return [undefined]
118
- #
119
- # @api private
120
- #
121
- def initialize(*)
122
- super
123
- @last_frame = nil
124
- end
125
-
126
127
  # Format start
127
128
  #
128
129
  # @param [Env] env
@@ -137,16 +138,14 @@ module Mutant
137
138
 
138
139
  # Format progress
139
140
  #
140
- # @param [Runner::Collector] collector
141
+ # @param [Runner::Status] status
141
142
  #
142
143
  # @return [String]
143
144
  #
144
145
  # @api private
145
146
  #
146
- def progress(collector)
147
- throttle do
148
- format(Printer::Collector, collector)
149
- end.to_s
147
+ def progress(status)
148
+ format(Printer::Status, status)
150
149
  end
151
150
 
152
151
  private
@@ -166,18 +165,6 @@ module Mutant
166
165
  buffer << tput.restore
167
166
  end
168
167
 
169
- # Call block throttled
170
- #
171
- # @return [self]
172
- #
173
- # @api private
174
- #
175
- def throttle
176
- now = Time.now
177
- return if @last_frame && (now - @last_frame) < OUTPUT_RATE
178
- yield.tap { @last_frame = now }
179
- end
180
-
181
168
  end # Framed
182
169
  end # Format
183
170
  end # CLI
@@ -5,6 +5,8 @@ module Mutant
5
5
  class Printer
6
6
  include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object)
7
7
 
8
+ delegate(:success?)
9
+
8
10
  NL = "\n".freeze
9
11
 
10
12
  # Run printer on object to output
@@ -98,16 +100,6 @@ module Mutant
98
100
  output.puts(string)
99
101
  end
100
102
 
101
- # Test if runner was successful
102
- #
103
- # @return [Boolean]
104
- #
105
- # @api private
106
- #
107
- def success?
108
- object.success?
109
- end
110
-
111
103
  # Colorize message
112
104
  #
113
105
  # @param [Color] color
@@ -142,8 +134,10 @@ module Mutant
142
134
  #
143
135
  alias_method :color?, :tty?
144
136
 
145
- # Printer for run collector
146
- class Collector < self
137
+ # Printer for runner status
138
+ class Status < self
139
+
140
+ delegate(:active_jobs, :env_result)
147
141
 
148
142
  # Print progress for collector
149
143
  #
@@ -152,14 +146,44 @@ module Mutant
152
146
  # @api private
153
147
  #
154
148
  def run
155
- visit(EnvProgress, object.result)
156
- active_subject_results = object.active_subject_results
149
+ visit(EnvProgress, object.env_result)
157
150
  info('Active subjects: %d', active_subject_results.length)
158
151
  visit_collection(SubjectProgress, active_subject_results)
152
+ job_status
159
153
  self
160
154
  end
161
155
 
162
- end # Collector
156
+ private
157
+
158
+ # Print worker status
159
+ #
160
+ # @return [undefined]
161
+ #
162
+ # @api private
163
+ #
164
+ def job_status
165
+ return if active_jobs.empty?
166
+ info('Active Jobs:')
167
+ object.active_jobs.sort_by(&:index).each do |job|
168
+ info('%d: %s', job.index, job.mutation.identification)
169
+ end
170
+ end
171
+
172
+ # Return active subject results
173
+ #
174
+ # @return [Array<Result::Subject>]
175
+ #
176
+ # @api private
177
+ #
178
+ def active_subject_results
179
+ active_subjects = active_jobs.map(&:mutation).flat_map(&:subject).to_set
180
+
181
+ env_result.subject_results.select do |subject_result|
182
+ active_subjects.include?(subject_result.subject)
183
+ end
184
+ end
185
+
186
+ end # Status
163
187
 
164
188
  # Progress printer for configuration
165
189
  class Config < self
@@ -406,9 +430,9 @@ module Mutant
406
430
  # Reporter for mutation results
407
431
  class MutationResult < self
408
432
 
409
- delegate :mutation, :failed_test_results
433
+ delegate :mutation, :test_result
410
434
 
411
- DIFF_ERROR_MESSAGE = 'BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!'.freeze
435
+ DIFF_ERROR_MESSAGE = 'BUG: Mutation NOT resulted in exactly one diff hunk. Please report a reproduction!'.freeze
412
436
 
413
437
  MAP = {
414
438
  Mutant::Mutation::Evil => :evil_details,
@@ -424,13 +448,13 @@ module Mutant
424
448
  "%s\n" \
425
449
  "Unparsed Source:\n" \
426
450
  "%s\n" \
427
- "Test Reports: %d\n"
451
+ "Test Result:\n".freeze
428
452
 
429
453
  NOOP_MESSAGE =
430
454
  "---- Noop failure -----\n" \
431
455
  "No code was inserted. And the test did NOT PASS.\n" \
432
456
  "This is typically a problem of your specs not passing unmutated.\n" \
433
- "Test Reports: %d\n"
457
+ "Test Result:\n".freeze
434
458
 
435
459
  FOOTER = '-----------------------'.freeze
436
460
 
@@ -479,8 +503,8 @@ module Mutant
479
503
  # @api private
480
504
  #
481
505
  def noop_details
482
- info(NOOP_MESSAGE, failed_test_results.length)
483
- visit_failed_test_results
506
+ info(NOOP_MESSAGE)
507
+ visit_test_result
484
508
  end
485
509
 
486
510
  # Neutral details
@@ -490,8 +514,8 @@ module Mutant
490
514
  # @api private
491
515
  #
492
516
  def neutral_details
493
- info(NEUTRAL_MESSAGE, mutation.subject.node.inspect, mutation.source, failed_test_results.length)
494
- visit_failed_test_results
517
+ info(NEUTRAL_MESSAGE, mutation.subject.node.inspect, mutation.source)
518
+ visit_test_result
495
519
  end
496
520
 
497
521
  # Visit failed test results
@@ -500,8 +524,8 @@ module Mutant
500
524
  #
501
525
  # @api private
502
526
  #
503
- def visit_failed_test_results
504
- visit_collection(TestResult, failed_test_results)
527
+ def visit_test_result
528
+ visit(TestResult, test_result)
505
529
  end
506
530
 
507
531
  end # MutationResult
@@ -509,7 +533,7 @@ module Mutant
509
533
  # Test result reporter
510
534
  class TestResult < self
511
535
 
512
- delegate :test, :runtime
536
+ delegate :tests, :runtime
513
537
 
514
538
  # Run test result reporter
515
539
  #
@@ -518,11 +542,26 @@ module Mutant
518
542
  # @api private
519
543
  #
520
544
  def run
521
- status('- %s / runtime: %s', test.identification, object.runtime)
545
+ status('- %d @ runtime: %s', tests.length, runtime)
546
+ tests.each do |test|
547
+ puts(" - #{test.identification}")
548
+ end
522
549
  puts('Test Output:')
523
550
  puts(object.output)
524
551
  end
525
552
 
553
+ # Test if test result is successful
554
+ #
555
+ # Only used to determine color.
556
+ #
557
+ # @return [false]
558
+ #
559
+ # @api private
560
+ #
561
+ def success?
562
+ false
563
+ end
564
+
526
565
  end # TestResult
527
566
  end # Printer
528
567
  end # CLI