mutiny 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 73bdf7c448948325e3ed64e73d07c14b1ea2788b
4
- data.tar.gz: aad759c85a854b070482db7e9d1775077df23162
3
+ metadata.gz: dd2e01d59dd079e28a3fa98029de8c55d4d5b705
4
+ data.tar.gz: 4b7b13dcbe3563b19d9bf7317f85b2fcebf0509b
5
5
  SHA512:
6
- metadata.gz: 634437f983d4893785bafe74e7b269fc41cc922f13dc4caabf79d6ffdc08d1f4e018d250865554ff1d3218e1d177f1039bd2af0bdd23c9869f2b46e1350b3d11
7
- data.tar.gz: 29d4deb64ad71594980a15ffc7e97b8214cb18cb18c37541cdef80b984a337e91c2fce0508020f707a8fedf1501a78b2ef019659b1dc15284d18ebc52f5e8bbb
6
+ metadata.gz: 8182f9ad54d073ca14bdbe0ce8d583e175551602aaa161fa3de5d1ee02f4bb7cd699f92f5a04ffdb1ea9abb07a80170e35e5045bdd604108353dd4aae84ecc10
7
+ data.tar.gz: 0bd7c7bcf5372e7fbafae2c824206957c3864e717c3aab0bf23458d5d7ec4fbade7446775dc552d27294aa8095ae9b9664d06561d33693f3e21f612d9de6b45d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mutiny (0.2.1)
4
+ mutiny (0.2.2)
5
5
  gli (~> 2.13.0)
6
6
  metamorpher (~> 0.2.2)
7
7
  parser (~> 2.2.2)
@@ -45,7 +45,7 @@ GEM
45
45
  ffi (1.9.10)
46
46
  gherkin (2.12.2)
47
47
  multi_json (~> 1.3)
48
- gli (2.13.1)
48
+ gli (2.13.2)
49
49
  ice_nine (0.11.1)
50
50
  json (1.8.3)
51
51
  memoizable (0.4.2)
@@ -110,4 +110,4 @@ DEPENDENCIES
110
110
  rubocop (~> 0.33.0)
111
111
 
112
112
  BUNDLED WITH
113
- 1.10.4
113
+ 1.10.6
data/RELEASES.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Release History
2
2
 
3
+ ## v0.2.2 (20 January 2016)
4
+ * Add rudimentary support for stillborn mutants
5
+ * Better error reporting during mutant generation
6
+ * Reduce chance that irrelevant subjects are selected for mutation
7
+
3
8
  ## v0.2.1 (20 August 2015)
4
9
  * Fix [#1](https://github.com/mutiny/mutiny/issues/1) - "Uninitialized constant" exception for all commands
5
10
 
data/bin/mutiny CHANGED
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
+ lib = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
2
5
  require "gli"
3
6
  require "mutiny"
4
7
 
5
8
  include GLI::App
6
9
 
10
+ version Mutiny::VERSION
7
11
  program_desc "A tiny mutation testing framework"
8
12
 
9
13
  flag [:l, :loads], type: Array, default_value: ['lib']
@@ -20,7 +20,7 @@ module Mutiny
20
20
  def analyse_all
21
21
  mutant_set.mutants.each do |mutant|
22
22
  mutant.apply
23
- test_run = integration.test(mutant.subject)
23
+ test_run = integration.test(mutant.subject) unless mutant.stillborn?
24
24
  results.add(mutant, test_run)
25
25
  end
26
26
  end
@@ -4,7 +4,7 @@ module Mutiny
4
4
  def add(mutant, test_run)
5
5
  mutants << mutant
6
6
  test_runs[mutant] = test_run
7
- killed << mutant if test_run.failed?
7
+ killed << mutant if mutant.stillborn? || test_run.failed?
8
8
  end
9
9
 
10
10
  def kill_count
@@ -21,5 +21,9 @@ module Mutiny
21
21
  def load_paths
22
22
  loads.map(&File.method(:expand_path))
23
23
  end
24
+
25
+ def can_load?(source_path)
26
+ load_paths.any? { |load_path| source_path.start_with?(load_path) }
27
+ end
24
28
  end
25
29
  end
@@ -26,11 +26,31 @@ module Mutiny
26
26
 
27
27
  def summarise(mutant)
28
28
  identifier = mutant.identifier
29
- status = results.survived?(mutant) ? "survived" : "killed"
30
- executed_count = results.test_run_for(mutant).executed_count
31
- total_count = results.test_run_for(mutant).tests.size
32
- runtime = results.test_run_for(mutant).runtime
33
- [identifier, status, "#{executed_count} (of #{total_count})", runtime]
29
+ status = status_for_mutant(mutant)
30
+ [identifier, status] + summarise_tests(mutant)
31
+ end
32
+
33
+ def status_for_mutant(mutant)
34
+ if mutant.stillborn?
35
+ "stillborn"
36
+ elsif results.survived?(mutant)
37
+ "survived"
38
+ else
39
+ "killed"
40
+ end
41
+ end
42
+
43
+ def summarise_tests(mutant)
44
+ if mutant.stillborn?
45
+ number_of_tests = "n/a"
46
+ runtime = "n/a"
47
+ else
48
+ executed_count = results.test_run_for(mutant).executed_count
49
+ total_count = results.test_run_for(mutant).tests.size
50
+ runtime = results.test_run_for(mutant).runtime
51
+ number_of_tests = "#{executed_count} (of #{total_count})"
52
+ end
53
+ [number_of_tests, runtime]
34
54
  end
35
55
 
36
56
  def results
@@ -3,20 +3,24 @@ require 'fileutils'
3
3
  module Mutiny
4
4
  module Mutants
5
5
  class Mutant
6
- attr_reader :subject, :code
6
+ attr_reader :subject, :code, :stillborn
7
+ alias_method :stillborn?, :stillborn
7
8
 
8
9
  def initialize(subject: nil, code:)
9
10
  @subject = subject
10
11
  @code = code
12
+ @stillborn = false
11
13
  end
12
14
 
13
15
  def apply
14
- # Evaluate the mutanted code, overidding any existing version.
16
+ # Evaluate the mutated code, overidding any existing version.
15
17
  # We evaluate in the context of TOPLEVEL_BINDING as we want
16
18
  # the unit to be evaluated in its usual namespace.
17
19
  # rubocop:disable Eval
18
20
  eval(code, TOPLEVEL_BINDING)
19
21
  # rubocop:enable Eval
22
+ rescue
23
+ @stillborn = true
20
24
  end
21
25
 
22
26
  def eql?(other)
@@ -7,8 +7,10 @@ module Mutiny
7
7
  extend Forwardable
8
8
  def_delegators :mutants, :size, :<<, :concat
9
9
 
10
- def mutants
11
- @mutants ||= []
10
+ attr_reader :mutants
11
+
12
+ def initialize(*mutants)
13
+ @mutants = mutants
12
14
  end
13
15
 
14
16
  def group_by_subject
@@ -27,6 +29,16 @@ module Mutiny
27
29
  ordered.each { |m| m.store(mutant_directory) }
28
30
  end
29
31
 
32
+ def eql?(other)
33
+ other.mutants == mutants
34
+ end
35
+
36
+ alias_method "==", "eql?"
37
+
38
+ def hash
39
+ mutants.hash
40
+ end
41
+
30
42
  class OrderedMutant < SimpleDelegator
31
43
  def initialize(mutant, number)
32
44
  super(mutant)
@@ -0,0 +1,10 @@
1
+ require "metamorpher"
2
+
3
+ module Mutiny
4
+ module Mutants
5
+ class Mutation
6
+ class Error < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -5,6 +5,10 @@ module Mutiny
5
5
  class Mutation
6
6
  include Metamorpher::Mutator
7
7
  include Metamorpher::Builders::AST
8
+
9
+ def name
10
+ self.class.name
11
+ end
8
12
  end
9
13
  end
10
14
  end
@@ -1,8 +1,11 @@
1
1
  require_relative "mutant_set"
2
+ require_relative "mutation/error"
2
3
 
3
4
  module Mutiny
4
5
  module Mutants
5
6
  class MutationSet
7
+ attr_reader :mutations
8
+
6
9
  def initialize(*mutations)
7
10
  @mutations = mutations
8
11
  end
@@ -12,19 +15,21 @@ module Mutiny
12
15
  # several mutation operators could be matched simulatenously during a single AST traversal
13
16
  def mutate(subjects)
14
17
  MutantSet.new.tap do |mutants|
15
- @mutations.each do |mutation|
16
- subjects.each do |subject|
17
- mutated_codes = mutation.mutate_file(subject.path)
18
- mutants.concat(create_mutants(subject, mutated_codes))
19
- end
18
+ subjects.product(mutations).each do |subject, mutation|
19
+ mutants.concat(mutate_one(subject, mutation))
20
20
  end
21
21
  end
22
22
  end
23
23
 
24
24
  private
25
25
 
26
- def create_mutants(subject, mutated_codes)
27
- mutated_codes.map { |code| Mutant.new(subject: subject, code: code) }
26
+ def mutate_one(subject, mutation)
27
+ mutation
28
+ .mutate_file(subject.path)
29
+ .map { |code| Mutant.new(subject: subject, code: code) }
30
+ rescue
31
+ msg = "Error encountered whilst mutating file at '#{subject.path}' with #{mutation.name}"
32
+ raise Mutation::Error, msg
28
33
  end
29
34
  end
30
35
  end
@@ -28,25 +28,26 @@ module Mutiny
28
28
  end
29
29
 
30
30
  def loadable?
31
- !load_path.nil?
31
+ !source_files.empty?
32
32
  end
33
33
 
34
34
  def load_path
35
35
  configuration.load_paths.detect do |load_path|
36
- source_locations.any? { |locs| locs.start_with?(load_path) }
36
+ source_files.any? { |locs| locs.start_with?(load_path) }
37
37
  end
38
38
  end
39
39
 
40
- def source_locations
40
+ def source_files
41
41
  mod.instance_methods(false)
42
42
  .map { |method_name| mod.instance_method(method_name).source_location }
43
43
  .reject(&:nil?)
44
44
  .map(&:first)
45
+ .select { |source_file| configuration.can_load?(source_file) }
45
46
  .uniq
46
47
  end
47
48
 
48
49
  def absolute_path
49
- source_locations.first
50
+ source_files.first
50
51
  end
51
52
  end
52
53
  end
@@ -1,16 +1,16 @@
1
1
  module Mutiny
2
2
  module Subjects
3
3
  class SubjectSet
4
+ include Enumerable
5
+ extend Forwardable
6
+ def_delegators :@subjects, :each, :product
7
+
4
8
  def initialize(subjects)
5
9
  @subjects = subjects
6
10
  end
7
11
 
8
12
  def names
9
- @names ||= @subjects.map(&:name).sort
10
- end
11
-
12
- def each(&block)
13
- @subjects.each(&block)
13
+ @names ||= map(&:name).sort
14
14
  end
15
15
  end
16
16
  end
@@ -1,3 +1,3 @@
1
1
  module Mutiny
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
data/lib/mutiny.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative "mutiny/version"
1
2
  require_relative "mutiny/integration"
2
3
  require_relative "mutiny/configuration"
3
4
  require_relative "mutiny/subjects"
data/spec/spec_helper.rb CHANGED
@@ -25,6 +25,12 @@ RSpec.configure do |config|
25
25
  # --seed 1234
26
26
  config.order = 'random'
27
27
 
28
+ # Ensure that mocks responds only to messages that the real object would also
29
+ # respond to
30
+ config.mock_with :rspec do |mocks|
31
+ mocks.verify_doubled_constant_names = true
32
+ end
33
+
28
34
  # Isolate mutiny-under-test from its own test suite
29
35
  config.around(:example) do |example|
30
36
  RSpec::Core::Sandbox.sandboxed do
@@ -0,0 +1,78 @@
1
+ module Mutiny
2
+ module Mutants
3
+ describe MutationSet do
4
+ it "produces one mutant for each result of mutation.mutate_file" do
5
+ mutation_set = MutationSet.new(mutation_for(:code1, :code2, :code3))
6
+ subjects = subjects_for(:path1)
7
+
8
+ expected = MutantSet.new(
9
+ Mutant.new(subject: subjects.first, code: :code1),
10
+ Mutant.new(subject: subjects.first, code: :code2),
11
+ Mutant.new(subject: subjects.first, code: :code3)
12
+ )
13
+
14
+ expect(mutation_set.mutate(subjects)).to eq(expected)
15
+ end
16
+
17
+ it "produces mutants from each mutation" do
18
+ mutation_set = MutationSet.new(mutation_for(:code1), mutation_for(:code2))
19
+ subjects = subjects_for(:path1)
20
+
21
+ expected = MutantSet.new(
22
+ Mutant.new(subject: subjects.first, code: :code1),
23
+ Mutant.new(subject: subjects.first, code: :code2)
24
+ )
25
+
26
+ expect(mutation_set.mutate(subjects)).to eq(expected)
27
+ end
28
+
29
+ it "produces mutants for each subject" do
30
+ mutation_set = MutationSet.new(mutation_for(:code1, :code2), mutation_for(:code3))
31
+ subjects = subjects_for(:path1, :path2)
32
+
33
+ expected = MutantSet.new(
34
+ Mutant.new(subject: subjects.first, code: :code1),
35
+ Mutant.new(subject: subjects.first, code: :code2),
36
+ Mutant.new(subject: subjects.first, code: :code3),
37
+ Mutant.new(subject: subjects.last, code: :code1),
38
+ Mutant.new(subject: subjects.last, code: :code2),
39
+ Mutant.new(subject: subjects.last, code: :code3)
40
+ )
41
+
42
+ expect(mutation_set.mutate(subjects)).to eq(expected)
43
+ end
44
+
45
+ it "produces a mutiny error when mutation fails" do
46
+ mutation = instance_double(Mutation)
47
+ allow(mutation).to receive(:name) { "BuggyMutation" }
48
+ allow(mutation).to receive(:mutate_file).and_raise("boom")
49
+ mutation_set = MutationSet.new(mutation)
50
+ subjects = subjects_for(:path1)
51
+
52
+ expect { mutation_set.mutate(subjects) }.to raise_error do |error|
53
+ expected_message = "Error encountered whilst mutating file at 'path1' with BuggyMutation"
54
+
55
+ expect(error).to be_a(Mutation::Error)
56
+ expect(error.message).to eq(expected_message)
57
+ expect(error.cause.message).to eq("boom")
58
+ end
59
+ end
60
+
61
+ def mutation_for(*codes)
62
+ instance_double(Mutation).tap do |mutation|
63
+ allow(mutation).to receive(:mutate_file) { codes }
64
+ end
65
+ end
66
+
67
+ def subjects_for(*paths)
68
+ paths.map { |path| subject_for(path) }
69
+ end
70
+
71
+ def subject_for(path)
72
+ instance_double(Subjects::Subject).tap do |subject|
73
+ allow(subject).to receive(:path) { path }
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutiny
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Louis Rose
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-20 00:00:00.000000000 Z
11
+ date: 2016-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -210,6 +210,7 @@ files:
210
210
  - lib/mutiny/mutants/mutant.rb
211
211
  - lib/mutiny/mutants/mutant_set.rb
212
212
  - lib/mutiny/mutants/mutation.rb
213
+ - lib/mutiny/mutants/mutation/error.rb
213
214
  - lib/mutiny/mutants/mutation/method/binary_arithmetic_operator_replacement.rb
214
215
  - lib/mutiny/mutants/mutation/method/conditional_operator_deletion.rb
215
216
  - lib/mutiny/mutants/mutation/method/conditional_operator_insertion.rb
@@ -252,6 +253,7 @@ files:
252
253
  - spec/unit/isolation_spec.rb
253
254
  - spec/unit/mutants/mutant_set_spec.rb
254
255
  - spec/unit/mutants/mutant_spec.rb
256
+ - spec/unit/mutants/mutation_set_spec.rb
255
257
  - spec/unit/mutants/mutations/method/binary_operator_replacement_spec.rb
256
258
  - spec/unit/mutants/mutations/method/conditional_operator_deletion_spec.rb
257
259
  - spec/unit/mutants/mutations/method/conditional_operator_insertion_spec.rb
@@ -306,6 +308,7 @@ test_files:
306
308
  - spec/unit/isolation_spec.rb
307
309
  - spec/unit/mutants/mutant_set_spec.rb
308
310
  - spec/unit/mutants/mutant_spec.rb
311
+ - spec/unit/mutants/mutation_set_spec.rb
309
312
  - spec/unit/mutants/mutations/method/binary_operator_replacement_spec.rb
310
313
  - spec/unit/mutants/mutations/method/conditional_operator_deletion_spec.rb
311
314
  - spec/unit/mutants/mutations/method/conditional_operator_insertion_spec.rb