mutant 0.6.7 → 0.7.1

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