mutiny 0.2.3 → 0.2.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/Gemfile.lock +1 -1
  4. data/RELEASES.md +4 -0
  5. data/bin/mutiny +5 -2
  6. data/lib/mutiny/analysis/analyser.rb +8 -14
  7. data/lib/mutiny/configuration.rb +23 -3
  8. data/lib/mutiny/integration/hook.rb +11 -0
  9. data/lib/mutiny/integration/rspec/hook.rb +32 -0
  10. data/lib/mutiny/integration/rspec/runner.rb +19 -9
  11. data/lib/mutiny/integration/rspec.rb +4 -2
  12. data/lib/mutiny/integration.rb +10 -3
  13. data/lib/mutiny/mode/check.rb +2 -2
  14. data/lib/mutiny/mode/mutate.rb +6 -2
  15. data/lib/mutiny/mode/score.rb +13 -2
  16. data/lib/mutiny/mode.rb +3 -2
  17. data/lib/mutiny/mutants/mutant/location.rb +27 -0
  18. data/lib/mutiny/mutants/mutant.rb +10 -3
  19. data/lib/mutiny/mutants/mutant_set.rb +2 -26
  20. data/lib/mutiny/mutants/mutation_set.rb +21 -7
  21. data/lib/mutiny/mutants/storage/file_store.rb +27 -0
  22. data/lib/mutiny/mutants/storage/mutant_file.rb +55 -0
  23. data/lib/mutiny/mutants/storage/mutant_file_contents.rb +33 -0
  24. data/lib/mutiny/mutants/storage/mutant_file_name.rb +32 -0
  25. data/lib/mutiny/mutants/storage/path.rb +31 -0
  26. data/lib/mutiny/mutants/storage.rb +27 -0
  27. data/lib/mutiny/tests/selection/default.rb +11 -0
  28. data/lib/mutiny/tests/test_set.rb +4 -4
  29. data/lib/mutiny/version.rb +1 -1
  30. data/spec/integration/mutate_spec.rb +21 -0
  31. data/spec/integration/score_cached_spec.rb +34 -0
  32. data/spec/integration/score_spec.rb +1 -1
  33. data/spec/unit/integration/rspec_spec.rb +45 -0
  34. data/spec/unit/mutants/mutant/location_spec.rb +23 -0
  35. data/spec/unit/mutants/storage/mutant_file_contents_spec.rb +49 -0
  36. data/spec/unit/mutants/storage/mutant_file_name_spec.rb +34 -0
  37. data/spec/unit/mutants/storage_spec.rb +46 -0
  38. data/spec/unit/{subjects → tests}/test_set_spec.rb +49 -7
  39. metadata +26 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 346e69f9b36cf9c2a778b2e62bd51571d171e0f1
4
- data.tar.gz: 7b08e6ec512f813849038ef15ef718d0b415bafd
3
+ metadata.gz: b3b90916fcddcbfd2b6d6b373927154110c8d1a0
4
+ data.tar.gz: f8d277e9dc277bdde93a449e8a10018be5c96859
5
5
  SHA512:
6
- metadata.gz: 0cdfbdd49e7349e67cc27fc48a28031a4fe3931952c1bdf13d823a99832b93836f0f59e91aae11d1aa6ffb05cc022d1d7d07d21b377cbf99fc457dbf94aef28e
7
- data.tar.gz: 0229bca2450718da9ed3484c5070fd8a6d524cca72392621582c63ef4e59a107b40d93698ec787913a2b4e454621a05e7b48916e356cd20a2e5de0beac5eed05
6
+ metadata.gz: 74a0c7772807505bc0d79f386fcd958d53ff1466cef6bb6053fe6ba2d978bf07b9fbfe17bb6db1c8243bf30a1750bcc9d8f8273fe902ab5360f62f305d45f7d8
7
+ data.tar.gz: 3180029d832460442147ff1623b4f535318a1226b1312d41232a736546475fce1f793827a0f16a4ae12e354a7f77d0ad3099f487f0fd21700b048c9a0f60e218
data/.travis.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2.2
3
+ - 2.2.3
4
4
  addons:
5
5
  code_climate:
6
6
  repo_token: 0297c93e8cb012ae60aa0760b795608f96c681e1493d98153d5cd7bd97e3b650
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mutiny (0.2.3)
4
+ mutiny (0.2.4)
5
5
  gli (~> 2.13.0)
6
6
  metamorpher (~> 0.2.2)
7
7
  parser (~> 2.2.2)
data/RELEASES.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Release History
2
2
 
3
+ ## v0.2.4 (10 February 2016)
4
+ * Add --cached switch to the score command, which loads mutants from disk rather than generating them anew.
5
+ * Various changes to improve extensibility (i.e., mutant storage, test selection) and capabilities (i.e., mutant location and test hooks).
6
+
3
7
  ## v0.2.3 (26 January 2016)
4
8
  * Add mutation name to each mutant written to disk
5
9
  * Update to Ruby 2.2.3
data/bin/mutiny CHANGED
@@ -39,8 +39,11 @@ end
39
39
  desc 'Calculates a mutation score for your project'
40
40
  long_desc 'Calculates a mutation score for your project and displays a list of surviving mutants'
41
41
  command :score do |c|
42
- c.action do
43
- Mutiny::Mode::Score.new(@configuration).run
42
+ cached_desc = 'Use the mutants in "./.mutants" rather than generating mutants before scoring'
43
+ c.switch [:c, :cached], desc: cached_desc, negatable: false
44
+ c.action do |_, options, _|
45
+ cached = options.fetch(:cached, false)
46
+ Mutiny::Mode::Score.new(@configuration, cached: cached).run
44
47
  end
45
48
  end
46
49
 
@@ -11,26 +11,20 @@ module Mutiny
11
11
  end
12
12
 
13
13
  def call
14
- analyse_all
15
- results
16
- end
17
-
18
- private
14
+ results = Results.new
19
15
 
20
- def analyse_all
21
16
  mutant_set.mutants.each do |mutant|
22
- mutant.apply
23
- test_run = integration.test(mutant.subject) unless mutant.stillborn?
24
- results.add(mutant, test_run)
17
+ results.add(mutant, analyse(mutant))
25
18
  end
26
- end
27
19
 
28
- def results
29
- @results ||= Results.new
20
+ results
30
21
  end
31
22
 
32
- def mutant_set
33
- @mutant_set ||= configuration.mutator.mutants_for(environment.subjects)
23
+ private
24
+
25
+ def analyse(mutant)
26
+ mutant.apply
27
+ mutant.stillborn? ? nil : integration.test(mutant)
34
28
  end
35
29
  end
36
30
  end
@@ -1,11 +1,13 @@
1
1
  require_relative 'pattern'
2
2
  require_relative 'reporter/stdout'
3
+ require_relative 'tests/selection/default'
3
4
  require_relative 'integration/rspec'
4
5
  require_relative 'mutants/ruby'
6
+ require_relative 'mutants/storage'
5
7
 
6
8
  module Mutiny
7
9
  class Configuration
8
- attr_reader :loads, :requires, :patterns, :reporter, :integration, :mutator
10
+ attr_reader :loads, :requires, :patterns, :reporter, :mutants, :tests
9
11
 
10
12
  def initialize(loads: [], requires: [], patterns: [])
11
13
  @loads = loads
@@ -14,8 +16,8 @@ module Mutiny
14
16
  @patterns.map!(&Pattern.method(:new))
15
17
 
16
18
  @reporter = Reporter::Stdout.new
17
- @integration = Integration::RSpec.new
18
- @mutator = Mutants::Ruby.new
19
+ @mutants = Mutants.new
20
+ @tests = Tests.new
19
21
  end
20
22
 
21
23
  def load_paths
@@ -25,5 +27,23 @@ module Mutiny
25
27
  def can_load?(source_path)
26
28
  load_paths.any? { |load_path| source_path.start_with?(load_path) }
27
29
  end
30
+
31
+ class Mutants
32
+ attr_reader :mutator, :storage
33
+
34
+ def initialize
35
+ @mutator = Mutiny::Mutants::Ruby.new
36
+ @storage = Mutiny::Mutants::Storage.new
37
+ end
38
+ end
39
+
40
+ class Tests
41
+ attr_reader :selection, :integration
42
+
43
+ def initialize
44
+ @selection = Mutiny::Tests::Selection::Default.new
45
+ @integration = Mutiny::Integration::RSpec.new(@selection)
46
+ end
47
+ end
28
48
  end
29
49
  end
@@ -0,0 +1,11 @@
1
+ module Mutiny
2
+ class Integration
3
+ class Hook
4
+ def before(_example)
5
+ end
6
+
7
+ def after(_example)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ module Mutiny
2
+ class Integration
3
+ class RSpec < self
4
+ class Hook
5
+ attr_reader :hook
6
+
7
+ def initialize(hook)
8
+ @hook = hook
9
+ end
10
+
11
+ def install(configuration)
12
+ configuration.reporter.register_listener(self, :example_started)
13
+ configuration.reporter.register_listener(self, :example_failed)
14
+ configuration.reporter.register_listener(self, :example_passed)
15
+ end
16
+
17
+ def example_started(notification)
18
+ example = notification.example
19
+ hook.before(example) unless example.pending? || example.skipped?
20
+ end
21
+
22
+ def example_failed(notification)
23
+ hook.after(notification.example)
24
+ end
25
+
26
+ def example_passed(notification)
27
+ hook.after(notification.example)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -9,9 +9,10 @@ module Mutiny
9
9
  extend Forwardable
10
10
  def_delegators :@context, :world, :runner, :configuration, :output
11
11
 
12
- def initialize(test_set, context = Context.new)
12
+ def initialize(test_set, context = Context.new, hooks = [])
13
13
  @test_set = test_set
14
14
  @context = context
15
+ @hooks = hooks
15
16
  end
16
17
 
17
18
  def call
@@ -28,9 +29,9 @@ module Mutiny
28
29
  end
29
30
 
30
31
  def prepare
32
+ install_hooks
31
33
  filter_examples
32
- configuration.reporter.register_listener(self, :example_passed)
33
- configuration.reporter.register_listener(self, :example_failed)
34
+ listen_for_example_results
34
35
  end
35
36
 
36
37
  def run
@@ -51,12 +52,8 @@ module Mutiny
51
52
  )
52
53
  end
53
54
 
54
- def example_passed(notification)
55
- @passed_examples << notification.example
56
- end
57
-
58
- def example_failed(notification)
59
- @failed_examples << notification.example
55
+ def install_hooks
56
+ @hooks.each { |hook| hook.install(configuration) }
60
57
  end
61
58
 
62
59
  def filter_examples
@@ -64,6 +61,19 @@ module Mutiny
64
61
  example.keep_if(&@test_set.examples.method(:include?))
65
62
  end
66
63
  end
64
+
65
+ def listen_for_example_results
66
+ configuration.reporter.register_listener(self, :example_passed)
67
+ configuration.reporter.register_listener(self, :example_failed)
68
+ end
69
+
70
+ def example_passed(notification)
71
+ @passed_examples << notification.example
72
+ end
73
+
74
+ def example_failed(notification)
75
+ @failed_examples << notification.example
76
+ end
67
77
  end
68
78
  end
69
79
  end
@@ -1,6 +1,7 @@
1
1
  require_relative "rspec/context"
2
2
  require_relative "rspec/parser"
3
3
  require_relative "rspec/runner"
4
+ require_relative "rspec/hook"
4
5
 
5
6
  module Mutiny
6
7
  class Integration
@@ -11,8 +12,9 @@ module Mutiny
11
12
  Parser.new(context(options)).call
12
13
  end
13
14
 
14
- def run(test_set, options = {})
15
- Runner.new(test_set, context(options)).call
15
+ def run(test_set, hooks: [], **options)
16
+ rspec_hooks = hooks.map { |hook| RSpec::Hook.new(hook) }
17
+ Runner.new(test_set, context(options), rspec_hooks).call
16
18
  end
17
19
 
18
20
  private
@@ -1,11 +1,18 @@
1
+ require_relative "tests/selection/default"
1
2
  require_relative "isolation"
2
3
 
3
4
  module Mutiny
4
5
  class Integration
5
- def test(subject)
6
+ attr_reader :test_selection
7
+
8
+ def initialize(test_selection = Tests::Selection::Default.new)
9
+ @test_selection = test_selection
10
+ end
11
+
12
+ def test(mutant)
6
13
  Isolation.call do
7
- test_set = tests.for(subject) # TODO: is this correctly minimal?
8
- run(test_set, fail_fast: true)
14
+ selected_tests = test_selection.for(mutant, from: tests)
15
+ run(selected_tests, fail_fast: true)
9
16
  end
10
17
  end
11
18
  end
@@ -51,11 +51,11 @@ module Mutiny
51
51
  end
52
52
 
53
53
  def test_set
54
- @test_set ||= configuration.integration.tests.for_all(environment.subjects)
54
+ @test_set ||= configuration.tests.integration.tests.for_subjects(environment.subjects)
55
55
  end
56
56
 
57
57
  def test_run
58
- @test_run ||= configuration.integration.run(test_set)
58
+ @test_run ||= configuration.tests.integration.run(test_set)
59
59
  end
60
60
  end
61
61
  end
@@ -17,12 +17,16 @@ module Mutiny
17
17
  end
18
18
 
19
19
  def store_mutants
20
- mutant_set.store
20
+ mutant_storage.save(mutant_set)
21
21
  report "Check the '.mutants' directory to browse the generated mutants."
22
22
  end
23
23
 
24
24
  def mutant_set
25
- @mutant_set ||= configuration.mutator.mutants_for(environment.subjects)
25
+ @mutant_set ||= configuration.mutants.mutator.mutants_for(environment.subjects)
26
+ end
27
+
28
+ def mutant_storage
29
+ @store ||= configuration.mutants.storage
26
30
  end
27
31
  end
28
32
  end
@@ -58,11 +58,22 @@ module Mutiny
58
58
  end
59
59
 
60
60
  def analyser
61
- Analysis::Analyser.new(mutant_set: mutant_set, integration: configuration.integration)
61
+ Analysis::Analyser.new(
62
+ mutant_set: mutant_set,
63
+ integration: configuration.tests.integration
64
+ )
62
65
  end
63
66
 
64
67
  def mutant_set
65
- @mutant_set ||= configuration.mutator.mutants_for(environment.subjects)
68
+ @mutant_set ||= initialize_mutant_set
69
+ end
70
+
71
+ def initialize_mutant_set
72
+ if options[:cached]
73
+ configuration.mutants.storage.load
74
+ else
75
+ configuration.mutants.mutator.mutants_for(environment.subjects)
76
+ end
66
77
  end
67
78
  end
68
79
  end
data/lib/mutiny/mode.rb CHANGED
@@ -4,11 +4,12 @@ require_relative "mode/score"
4
4
 
5
5
  module Mutiny
6
6
  class Mode
7
- attr_reader :configuration, :environment
7
+ attr_reader :configuration, :environment, :options
8
8
 
9
- def initialize(configuration)
9
+ def initialize(configuration, **options)
10
10
  @configuration = configuration
11
11
  @environment = Subjects::Environment.new(configuration)
12
+ @options = options
12
13
  end
13
14
 
14
15
  private
@@ -0,0 +1,27 @@
1
+ module Mutiny
2
+ module Mutants
3
+ class Mutant
4
+ class Location
5
+ attr_reader :position, :content
6
+
7
+ def initialize(position:, content:)
8
+ @position = position.freeze
9
+ @content = content
10
+ end
11
+
12
+ def lines
13
+ Range.new(
14
+ line_number_of_offset(position.begin),
15
+ line_number_of_offset(position.end)
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ def line_number_of_offset(offset)
22
+ content[0..offset].lines.size
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,16 +1,23 @@
1
- require 'fileutils'
1
+ require_relative "mutant/location"
2
2
 
3
3
  module Mutiny
4
4
  module Mutants
5
5
  class Mutant
6
- attr_reader :subject, :code, :mutation_name, :stillborn
6
+ attr_reader :subject, :code, :mutation_name, :stillborn, :location
7
+ attr_accessor :index
7
8
  alias_method :stillborn?, :stillborn
8
9
 
9
- def initialize(subject: nil, code:, mutation_name: nil)
10
+ def initialize(subject: nil, code:, mutation_name: nil, index: nil, position: nil)
10
11
  @subject = subject
11
12
  @code = code
12
13
  @mutation_name = mutation_name
14
+ @index = index
13
15
  @stillborn = false
16
+ @location = Location.new(position: position, content: code)
17
+ end
18
+
19
+ def identifier
20
+ subject.relative_path.sub(/\.rb$/, ".#{index}.rb")
14
21
  end
15
22
 
16
23
  def apply
@@ -20,15 +20,12 @@ module Mutiny
20
20
  def ordered
21
21
  group_by_subject.flat_map do |_, mutants|
22
22
  mutants.map.with_index do |mutant, index|
23
- OrderedMutant.new(mutant, index)
23
+ mutant.index ||= index
24
+ mutant
24
25
  end
25
26
  end
26
27
  end
27
28
 
28
- def store(mutant_directory = ".mutants")
29
- ordered.each { |m| m.store(mutant_directory) }
30
- end
31
-
32
29
  def eql?(other)
33
30
  other.mutants == mutants
34
31
  end
@@ -38,27 +35,6 @@ module Mutiny
38
35
  def hash
39
36
  mutants.hash
40
37
  end
41
-
42
- class OrderedMutant < SimpleDelegator
43
- def initialize(mutant, number)
44
- super(mutant)
45
- @number = number
46
- end
47
-
48
- def identifier
49
- subject.relative_path.sub(/\.rb$/, ".#{@number}.rb")
50
- end
51
-
52
- def store(directory)
53
- path = File.join(directory, identifier)
54
- FileUtils.mkdir_p(File.dirname(path))
55
- File.open(path, 'w') { |f| f.write(serialised) }
56
- end
57
-
58
- def serialised
59
- "# " + mutation_name + "\n" + code
60
- end
61
- end
62
38
  end
63
39
  end
64
40
  end
@@ -14,23 +14,37 @@ module Mutiny
14
14
  # Probably could improve (more) if metamorpher also supported composite transformers so that
15
15
  # several mutation operators could be matched simulatenously during a single AST traversal
16
16
  def mutate(subjects)
17
- MutantSet.new.tap do |mutants|
18
- subjects.product(mutations).each do |subject, mutation|
19
- mutants.concat(mutate_one(subject, mutation))
20
- end
17
+ mutants = MutantSet.new
18
+ subjects.product(mutations).each do |subject, mutation|
19
+ mutants.concat(mutate_one(subject, mutation))
21
20
  end
21
+ mutants
22
22
  end
23
23
 
24
24
  private
25
25
 
26
26
  def mutate_one(subject, mutation)
27
- safely_mutate_file(subject.path, mutation).map do |code|
28
- Mutant.new(subject: subject, code: code, mutation_name: mutation.short_name)
27
+ safely_mutate_file(subject.path, mutation).map do |code, position|
28
+ Mutant.new(
29
+ subject: subject,
30
+ mutation_name: mutation.short_name,
31
+ code: code,
32
+ position: position
33
+ )
29
34
  end
30
35
  end
31
36
 
32
37
  def safely_mutate_file(path, mutation)
33
- mutation.mutate_file(path)
38
+ positions = []
39
+
40
+ code = mutation.mutate_file(path) do |change|
41
+ starting_position = change.original_position.begin
42
+ ending_position = change.original_position.begin + change.transformed_code.size - 1
43
+
44
+ positions << (starting_position..ending_position)
45
+ end
46
+
47
+ code.zip(positions)
34
48
  rescue
35
49
  msg = "Error encountered whilst mutating file at '#{path}' with #{mutation.name}"
36
50
  raise Mutation::Error, msg
@@ -0,0 +1,27 @@
1
+ require_relative "mutant_file"
2
+
3
+ module Mutiny
4
+ module Mutants
5
+ class Storage
6
+ class FileStore
7
+ attr_reader :mutant_directory, :strategy
8
+
9
+ def initialize(mutant_directory: ".mutants")
10
+ @mutant_directory = mutant_directory
11
+ @strategy = MutantFile.new(mutant_directory)
12
+ end
13
+
14
+ def save_all(mutants)
15
+ mutants.ordered.each do |mutant|
16
+ strategy.store(mutant)
17
+ end
18
+ end
19
+
20
+ def load_all
21
+ absolute_paths = Dir.glob(File.join(mutant_directory, "**", "*.rb"))
22
+ absolute_paths.map { |path| strategy.load(path) }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
1
+ require "fileutils"
2
+ require_relative "path"
3
+ require_relative "mutant_file_name"
4
+ require_relative "mutant_file_contents"
5
+
6
+ module Mutiny
7
+ module Mutants
8
+ class Storage
9
+ class MutantFile
10
+ attr_reader :mutant_directory
11
+
12
+ def initialize(mutant_directory)
13
+ @mutant_directory = mutant_directory
14
+ end
15
+
16
+ def load(absolute_path)
17
+ path = Path.from_absolute(path: absolute_path, root: mutant_directory)
18
+ deserialise
19
+ .merge(deserialised_contents(path)) { |_, left, right| left.merge(right) }
20
+ .merge(deserialised_filename(path)) { |_, left, right| left.merge(right) }
21
+ end
22
+
23
+ def store(mutant)
24
+ path = Path.from_relative(root: mutant_directory, path: filename.serialise(mutant))
25
+ FileUtils.mkdir_p(File.dirname(path.absolute))
26
+ File.open(path.absolute, 'w') { |f| f.write(contents.serialise(mutant)) }
27
+ end
28
+
29
+ private
30
+
31
+ def deserialise
32
+ {
33
+ subject: { root: mutant_directory }
34
+ }
35
+ end
36
+
37
+ def deserialised_filename(path)
38
+ filename.deserialise(path.relative)
39
+ end
40
+
41
+ def deserialised_contents(path)
42
+ contents.deserialise(File.read(path.absolute))
43
+ end
44
+
45
+ def filename
46
+ @filename ||= MutantFileName.new
47
+ end
48
+
49
+ def contents
50
+ @contents ||= MutantFileContents.new
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,33 @@
1
+ module Mutiny
2
+ module Mutants
3
+ class Storage
4
+ class MutantFileContents
5
+ def serialise(mutant)
6
+ "# " + mutant.subject.name + "\n" \
7
+ "# " + mutant.mutation_name + "\n" \
8
+ "# " + mutant.location.position.to_s + "\n" +
9
+ mutant.code
10
+ end
11
+
12
+ def deserialise(contents)
13
+ {
14
+ subject: { name: extract_contents_of_comment(contents.lines[0]) },
15
+ mutation_name: extract_contents_of_comment(contents.lines[1]),
16
+ position: convert_to_range(extract_contents_of_comment(contents.lines[2])),
17
+ code: contents.lines.drop(3).join
18
+ }
19
+ end
20
+
21
+ private
22
+
23
+ def extract_contents_of_comment(line)
24
+ line[2..-1].strip
25
+ end
26
+
27
+ def convert_to_range(string)
28
+ Range.new(*string.split("..").map(&:to_i))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,32 @@
1
+ module Mutiny
2
+ module Mutants
3
+ class Storage
4
+ class MutantFileName
5
+ def serialise(mutant)
6
+ path_with_index(mutant.subject.relative_path, mutant.index)
7
+ end
8
+
9
+ def deserialise(path)
10
+ {
11
+ subject: { path: path_without_index(path) },
12
+ index: index_of(path)
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def path_with_index(path, index)
19
+ path.sub(/\.rb$/, ".#{index}.rb")
20
+ end
21
+
22
+ def path_without_index(path)
23
+ path.sub(/\.\d+\.rb$/, ".rb")
24
+ end
25
+
26
+ def index_of(path)
27
+ path.match(/.*\.(\d+)\.rb/)[1].to_i
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "mutant_file"
2
+
3
+ module Mutiny
4
+ module Mutants
5
+ class Storage
6
+ class Path
7
+ def self.from_absolute(path:, root:)
8
+ relative_path = Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
9
+ new(relative_path, root)
10
+ end
11
+
12
+ def self.from_relative(path:, root:)
13
+ new(path, root)
14
+ end
15
+
16
+ attr_reader :relative, :root
17
+
18
+ def absolute
19
+ File.join(root, relative)
20
+ end
21
+
22
+ private
23
+
24
+ def initialize(relative, root)
25
+ @relative = relative
26
+ @root = root
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "storage/file_store"
2
+
3
+ module Mutiny
4
+ module Mutants
5
+ class Storage
6
+ attr_accessor :store
7
+
8
+ def initialize(mutant_directory: ".mutants", store: nil)
9
+ @store = store || FileStore.new(mutant_directory: mutant_directory)
10
+ end
11
+
12
+ def save(mutants)
13
+ store.save_all(mutants)
14
+ end
15
+
16
+ def load
17
+ mutants = store.load_all.map do |mutant_specification|
18
+ subject = Subjects::Subject.new(**mutant_specification[:subject])
19
+ mutant_specification[:subject] = subject
20
+ Mutant.new(**mutant_specification)
21
+ end
22
+
23
+ MutantSet.new(*mutants)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ module Mutiny
2
+ module Tests
3
+ module Selection
4
+ class Default
5
+ def for(mutant, from:)
6
+ from.for_subject(mutant.subject) # TODO: is this correctly minimal?
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -18,12 +18,12 @@ module Mutiny
18
18
  tests.map(&:location)
19
19
  end
20
20
 
21
- def for_all(subject_set)
22
- subset { |test| subject_set.names.include?(test.expression) }
21
+ def for_subject(subject)
22
+ subset { |test| subject.name == test.expression }
23
23
  end
24
24
 
25
- def for(subject)
26
- subset { |test| subject.name == test.expression }
25
+ def for_subjects(subjects)
26
+ subset { |test| subjects.names.include?(test.expression) }
27
27
  end
28
28
 
29
29
  def subset(&block)
@@ -1,3 +1,3 @@
1
1
  module Mutiny
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
@@ -53,4 +53,25 @@ describe "Using Mutiny to generate mutants" do
53
53
  check_file_content(".mutants/calculator/max.5.rb", /# RelationalOperatorReplacement/)
54
54
  check_file_content(".mutants/calculator/max.6.rb", /# RelationalOperatorReplacement/)
55
55
  end
56
+
57
+ it "should write position to each mutant" do
58
+ cd "calculator"
59
+ run "bundle exec mutiny mutate"
60
+
61
+ check_file_content(".mutants/calculator/min.0.rb", /# 93..96/)
62
+ check_file_content(".mutants/calculator/min.1.rb", /# 93..97/)
63
+ check_file_content(".mutants/calculator/min.2.rb", /# 93..105/)
64
+ check_file_content(".mutants/calculator/min.3.rb", /# 93..105/)
65
+ check_file_content(".mutants/calculator/min.4.rb", /# 93..105/)
66
+ check_file_content(".mutants/calculator/min.5.rb", /# 93..105/)
67
+ check_file_content(".mutants/calculator/min.6.rb", /# 93..104/)
68
+
69
+ check_file_content(".mutants/calculator/max.0.rb", /# 93..96/)
70
+ check_file_content(".mutants/calculator/max.1.rb", /# 93..97/)
71
+ check_file_content(".mutants/calculator/max.2.rb", /# 93..104/)
72
+ check_file_content(".mutants/calculator/max.3.rb", /# 93..105/)
73
+ check_file_content(".mutants/calculator/max.4.rb", /# 93..105/)
74
+ check_file_content(".mutants/calculator/max.5.rb", /# 93..105/)
75
+ check_file_content(".mutants/calculator/max.6.rb", /# 93..105/)
76
+ end
56
77
  end
@@ -0,0 +1,34 @@
1
+ describe "Using Mutiny to score existing mutants" do
2
+ before(:each) do
3
+ cd "calculator"
4
+ run "bundle exec mutiny mutate"
5
+
6
+ run "rm -rf .mutants/calculator/max.0.rb"
7
+ run "rm -rf .mutants/calculator/max.1.rb"
8
+ run "rm -rf .mutants/calculator/max.2.rb"
9
+ run "rm -rf .mutants/calculator/max.3.rb"
10
+ run "rm -rf .mutants/calculator/max.4.rb"
11
+ run "rm -rf .mutants/calculator/max.5.rb"
12
+ run "rm -rf .mutants/calculator/max.6.rb"
13
+
14
+ run "rm -rf .mutants/calculator/min.0.rb"
15
+ run "rm -rf .mutants/calculator/min.1.rb"
16
+
17
+ run "bundle exec mutiny score --cached"
18
+ end
19
+
20
+ it "should report a mutation score" do
21
+ expected_output = "Scoring...\n" \
22
+ "5 mutants, 4 killed\n"
23
+
24
+ expect(all_output).to include(expected_output)
25
+ end
26
+
27
+ it "should report status of mutants" do
28
+ expect(all_output).to include("calculator/min.2.rb | survived")
29
+ expect(all_output).to include("calculator/min.3.rb | killed")
30
+ expect(all_output).to include("calculator/min.4.rb | killed")
31
+ expect(all_output).to include("calculator/min.5.rb | killed")
32
+ expect(all_output).to include("calculator/min.6.rb | killed")
33
+ end
34
+ end
@@ -1,4 +1,4 @@
1
- describe "Using Mutiny to generate mutants" do
1
+ describe "Using Mutiny to score mutants" do
2
2
  before(:each) do
3
3
  cd "calculator"
4
4
  run "bundle exec mutiny mutate"
@@ -0,0 +1,45 @@
1
+ require "mutiny/integration/hook"
2
+
3
+ module Mutiny
4
+ class Integration
5
+ describe RSpec do
6
+ let(:test_set) { subject.tests }
7
+
8
+ it "should call hooks before each spec" do
9
+ in_example_project("calculator") do
10
+ hook = TestHook.new
11
+ subject.run(test_set, hooks: [hook])
12
+
13
+ expect(hook.examples_started.size).to eq(test_set.size)
14
+ end
15
+ end
16
+
17
+ it "should call hooks after each spec" do
18
+ in_example_project("calculator") do
19
+ hook = TestHook.new
20
+ subject.run(test_set, hooks: [hook])
21
+
22
+ expect(hook.examples_finished.size).to eq(test_set.size)
23
+ end
24
+ end
25
+
26
+ class TestHook < Hook
27
+ def before(example)
28
+ examples_started << example
29
+ end
30
+
31
+ def after(example)
32
+ examples_finished << example
33
+ end
34
+
35
+ def examples_started
36
+ @examples_started ||= []
37
+ end
38
+
39
+ def examples_finished
40
+ @examples_finished ||= []
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ module Mutiny
2
+ module Mutants
3
+ class Mutant
4
+ describe Location do
5
+ context "calculates lines" do
6
+ it "correctly for a multi-line location" do
7
+ # 0 1 2
8
+ # 01234567 89012345678901 234
9
+ location = Location.new(position: 3..21, content: "if BOMB\n raise 'boom'\nend")
10
+
11
+ expect(location.lines).to eq(1..2)
12
+ end
13
+
14
+ it "correctly for a single-line location" do
15
+ location = Location.new(position: 2..4, content: "a <= b\nb <= c")
16
+
17
+ expect(location.lines).to eq(1..1)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ module Mutiny
2
+ module Mutants
3
+ class Storage
4
+ describe MutantFileContents do
5
+ it "serialises" do
6
+ expect(subject.serialise(mutant)).to eq(serialised_mutant)
7
+ end
8
+
9
+ it "deserialises" do
10
+ expect(subject.deserialise(serialised_mutant)).to eq(deserialised_mutant)
11
+ end
12
+
13
+ def mutant
14
+ Mutant.new(
15
+ subject: subject_of_mutation,
16
+ code: "2 - 2",
17
+ index: 0,
18
+ mutation_name: "BAOR",
19
+ position: 2..3
20
+ )
21
+ end
22
+
23
+ def subject_of_mutation
24
+ Subjects::Subject.new(
25
+ name: "Two",
26
+ path: "~/Code/sums/two.rb",
27
+ root: "~/Code/sums"
28
+ )
29
+ end
30
+
31
+ def serialised_mutant
32
+ "# Two\n" \
33
+ "# BAOR\n" \
34
+ "# 2..3\n" \
35
+ "2 - 2"
36
+ end
37
+
38
+ def deserialised_mutant
39
+ {
40
+ subject: { name: "Two" },
41
+ mutation_name: "BAOR",
42
+ code: "2 - 2",
43
+ position: 2..3
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,34 @@
1
+ module Mutiny
2
+ module Mutants
3
+ class Storage
4
+ describe MutantFileName do
5
+ it "serialises" do
6
+ expect(subject.serialise(mutant)).to eq(serialised_mutant)
7
+ end
8
+
9
+ it "deserialises" do
10
+ expect(subject.deserialise(serialised_mutant)).to eq(deserialised_mutant)
11
+ end
12
+
13
+ def mutant
14
+ Mutant.new(subject: subject_of_mutation, code: "2 - 2", index: 10, mutation_name: "BAOR")
15
+ end
16
+
17
+ def subject_of_mutation
18
+ Subjects::Subject.new(name: "Two", path: "~/Code/sums/two.rb", root: "~/Code/sums")
19
+ end
20
+
21
+ def serialised_mutant
22
+ "two.10.rb"
23
+ end
24
+
25
+ def deserialised_mutant
26
+ {
27
+ subject: { path: "two.rb" },
28
+ index: 10
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,46 @@
1
+ module Mutiny
2
+ module Mutants
3
+ describe Storage do
4
+ it "transforms hashes to mutants and subjects" do
5
+ store = instance_double(Storage::FileStore)
6
+ allow(store).to receive(:load_all).and_return(loaded_data)
7
+
8
+ storage = Storage.new(store: store)
9
+ expect(storage.load).to eq(expected_mutant_set)
10
+ end
11
+
12
+ # rubocop:disable MethodLength
13
+ def loaded_data
14
+ [
15
+ {
16
+ subject: { name: "Two", path: "two.rb", root: "~/Code/sums" },
17
+ code: "2 - 2",
18
+ index: 0
19
+ },
20
+ {
21
+ subject: { name: "Two", path: "two.rb", root: "~/Code/sums" },
22
+ code: "2 * 2",
23
+ index: 1
24
+ },
25
+ {
26
+ subject: { name: "Four", path: "four.rb", root: "~/Code/sums" },
27
+ code: "4 - 4",
28
+ index: 0
29
+ }
30
+ ]
31
+ end
32
+ # rubocop:enable MethodLength
33
+
34
+ def expected_mutant_set
35
+ first_subject = Subjects::Subject.new(name: "Two", path: "two.rb", root: "~/Code/sums")
36
+ second_subject = Subjects::Subject.new(name: "Four", path: "four.rb", root: "~/Code/sums")
37
+
38
+ MutantSet.new(
39
+ Mutant.new(subject: first_subject, code: "2 - 2", index: 0),
40
+ Mutant.new(subject: first_subject, code: "2 * 2", index: 1),
41
+ Mutant.new(subject: second_subject, code: "4 - 4", index: 2)
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -33,42 +33,84 @@ module Mutiny
33
33
  end
34
34
  end
35
35
 
36
- context "for" do
36
+ context "for all" do
37
37
  it "should return only those tests (whose expression) matches a subject" do
38
38
  subjects = subject_set_for("Max", "Min")
39
39
  test_set = test_set_for("Subtract", "Min", "Add")
40
+ selected = test_set.for_subjects(subjects)
40
41
 
41
- expect(test_set.for_all(subjects)).to eq(test_set.subset { |t| t.expression == "Min" })
42
+ expect(selected).to eq(test_set.subset { |t| t.expression == "Min" })
42
43
  end
43
44
 
44
45
  it "should return multiple tests for a single subject" do
45
46
  subjects = subject_set_for("Min")
46
47
  test_set = test_set_for("Min", "Max", "Min", "Max", "Min")
48
+ selected = test_set.for_subjects(subjects)
47
49
 
48
- expect(test_set.for_all(subjects)).to eq(test_set.subset { |t| t.expression == "Min" })
50
+ expect(selected).to eq(test_set.subset { |t| t.expression == "Min" })
49
51
  end
50
52
 
51
53
  it "should return no tests when there are no tests" do
52
54
  subjects = subject_set_for("Max", "Min")
53
55
  test_set = TestSet.empty
56
+ selected = test_set.for_subjects(subjects)
54
57
 
55
- expect(test_set.for_all(subjects)).to eq(TestSet.empty)
58
+ expect(selected).to eq(TestSet.empty)
56
59
  end
57
60
 
58
61
  it "should return no tests when there are no relevant subjects" do
59
62
  subjects = subject_set_for("Max", "Min")
60
63
  test_set = test_set_for("Subtract", "Add")
64
+ selected = test_set.for_subjects(subjects)
61
65
 
62
- expect(test_set.for_all(subjects)).to eq(TestSet.empty)
66
+ expect(selected).to eq(TestSet.empty)
63
67
  end
64
68
 
65
69
  def subject_set_for(*names)
66
70
  Subjects::SubjectSet.new(names.map { |n| Subjects::Subject.new(name: n) })
67
71
  end
72
+ end
73
+
74
+ context "for" do
75
+ it "should return only those tests (whose expression) matches the mutant's subject" do
76
+ subject = subject_for("Min")
77
+ test_set = test_set_for("Subtract", "Min", "Add")
78
+ selected = test_set.for_subject(subject)
79
+
80
+ expect(selected).to eq(test_set.subset { |t| t.expression == "Min" })
81
+ end
82
+
83
+ it "should return multiple tests for a single mutant" do
84
+ subject = subject_for("Min")
85
+ test_set = test_set_for("Min", "Max", "Min", "Max", "Min")
86
+ selected = test_set.for_subject(subject)
87
+
88
+ expect(selected).to eq(test_set.subset { |t| t.expression == "Min" })
89
+ end
68
90
 
69
- def test_set_for(*expressions)
70
- TestSet.new(expressions.map { |e| Test.new(expression: e) })
91
+ it "should return no tests when there are no tests" do
92
+ subject = subject_for("Min")
93
+ test_set = TestSet.empty
94
+ selected = test_set.for_subject(subject)
95
+
96
+ expect(selected).to eq(TestSet.empty)
71
97
  end
98
+
99
+ it "should return no tests when mutant is irrelevant" do
100
+ subject = subject_for("Min")
101
+ test_set = test_set_for("Subtract", "Add")
102
+ selected = test_set.for_subject(subject)
103
+
104
+ expect(selected).to eq(TestSet.empty)
105
+ end
106
+
107
+ def subject_for(name)
108
+ Subjects::Subject.new(name: name)
109
+ end
110
+ end
111
+
112
+ def test_set_for(*expressions)
113
+ TestSet.new(expressions.map { |e| Test.new(expression: e) })
72
114
  end
73
115
  end
74
116
  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.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Louis Rose
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-26 00:00:00.000000000 Z
11
+ date: 2016-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -194,8 +194,10 @@ files:
194
194
  - lib/mutiny/analysis/results.rb
195
195
  - lib/mutiny/configuration.rb
196
196
  - lib/mutiny/integration.rb
197
+ - lib/mutiny/integration/hook.rb
197
198
  - lib/mutiny/integration/rspec.rb
198
199
  - lib/mutiny/integration/rspec/context.rb
200
+ - lib/mutiny/integration/rspec/hook.rb
199
201
  - lib/mutiny/integration/rspec/parser.rb
200
202
  - lib/mutiny/integration/rspec/runner.rb
201
203
  - lib/mutiny/integration/rspec/test.rb
@@ -208,6 +210,7 @@ files:
208
210
  - lib/mutiny/mode/mutate.rb
209
211
  - lib/mutiny/mode/score.rb
210
212
  - lib/mutiny/mutants/mutant.rb
213
+ - lib/mutiny/mutants/mutant/location.rb
211
214
  - lib/mutiny/mutants/mutant_set.rb
212
215
  - lib/mutiny/mutants/mutation.rb
213
216
  - lib/mutiny/mutants/mutation/error.rb
@@ -227,6 +230,12 @@ files:
227
230
  - lib/mutiny/mutants/mutation/method/unary_arithmetic_operator_insertion.rb
228
231
  - lib/mutiny/mutants/mutation_set.rb
229
232
  - lib/mutiny/mutants/ruby.rb
233
+ - lib/mutiny/mutants/storage.rb
234
+ - lib/mutiny/mutants/storage/file_store.rb
235
+ - lib/mutiny/mutants/storage/mutant_file.rb
236
+ - lib/mutiny/mutants/storage/mutant_file_contents.rb
237
+ - lib/mutiny/mutants/storage/mutant_file_name.rb
238
+ - lib/mutiny/mutants/storage/path.rb
230
239
  - lib/mutiny/output/table.rb
231
240
  - lib/mutiny/pattern.rb
232
241
  - lib/mutiny/reporter/stdout.rb
@@ -236,6 +245,7 @@ files:
236
245
  - lib/mutiny/subjects/subject.rb
237
246
  - lib/mutiny/subjects/subject_set.rb
238
247
  - lib/mutiny/tests.rb
248
+ - lib/mutiny/tests/selection/default.rb
239
249
  - lib/mutiny/tests/test.rb
240
250
  - lib/mutiny/tests/test_run.rb
241
251
  - lib/mutiny/tests/test_set.rb
@@ -243,6 +253,7 @@ files:
243
253
  - mutiny.gemspec
244
254
  - spec/integration/check_spec.rb
245
255
  - spec/integration/mutate_spec.rb
256
+ - spec/integration/score_cached_spec.rb
246
257
  - spec/integration/score_spec.rb
247
258
  - spec/spec_helper.rb
248
259
  - spec/support/aruba.rb
@@ -250,7 +261,9 @@ files:
250
261
  - spec/support/shared_examples/shared_examples_for_an_operator_replacement_mutation.rb
251
262
  - spec/unit/integration/rspec/parser_spec.rb
252
263
  - spec/unit/integration/rspec/runner_spec.rb
264
+ - spec/unit/integration/rspec_spec.rb
253
265
  - spec/unit/isolation_spec.rb
266
+ - spec/unit/mutants/mutant/location_spec.rb
254
267
  - spec/unit/mutants/mutant_set_spec.rb
255
268
  - spec/unit/mutants/mutant_spec.rb
256
269
  - spec/unit/mutants/mutation_set_spec.rb
@@ -266,11 +279,14 @@ files:
266
279
  - spec/unit/mutants/mutations/method/shortcut_assignment_operator_replacement_spec.rb
267
280
  - spec/unit/mutants/mutations/method/unary_arithmetic_operator_deletion_spec.rb
268
281
  - spec/unit/mutants/mutations/method/unary_arithmetic_operator_insertion_spec.rb
282
+ - spec/unit/mutants/storage/mutant_file_contents_spec.rb
283
+ - spec/unit/mutants/storage/mutant_file_name_spec.rb
284
+ - spec/unit/mutants/storage_spec.rb
269
285
  - spec/unit/pattern_spec.rb
270
286
  - spec/unit/subjects/environment/type_spec.rb
271
287
  - spec/unit/subjects/environment_spec.rb
272
288
  - spec/unit/subjects/subject_spec.rb
273
- - spec/unit/subjects/test_set_spec.rb
289
+ - spec/unit/tests/test_set_spec.rb
274
290
  homepage: https://github.com/mutiny/mutiny
275
291
  licenses:
276
292
  - MIT
@@ -298,6 +314,7 @@ summary: A tiny mutation testing framework for Ruby
298
314
  test_files:
299
315
  - spec/integration/check_spec.rb
300
316
  - spec/integration/mutate_spec.rb
317
+ - spec/integration/score_cached_spec.rb
301
318
  - spec/integration/score_spec.rb
302
319
  - spec/spec_helper.rb
303
320
  - spec/support/aruba.rb
@@ -305,7 +322,9 @@ test_files:
305
322
  - spec/support/shared_examples/shared_examples_for_an_operator_replacement_mutation.rb
306
323
  - spec/unit/integration/rspec/parser_spec.rb
307
324
  - spec/unit/integration/rspec/runner_spec.rb
325
+ - spec/unit/integration/rspec_spec.rb
308
326
  - spec/unit/isolation_spec.rb
327
+ - spec/unit/mutants/mutant/location_spec.rb
309
328
  - spec/unit/mutants/mutant_set_spec.rb
310
329
  - spec/unit/mutants/mutant_spec.rb
311
330
  - spec/unit/mutants/mutation_set_spec.rb
@@ -321,8 +340,11 @@ test_files:
321
340
  - spec/unit/mutants/mutations/method/shortcut_assignment_operator_replacement_spec.rb
322
341
  - spec/unit/mutants/mutations/method/unary_arithmetic_operator_deletion_spec.rb
323
342
  - spec/unit/mutants/mutations/method/unary_arithmetic_operator_insertion_spec.rb
343
+ - spec/unit/mutants/storage/mutant_file_contents_spec.rb
344
+ - spec/unit/mutants/storage/mutant_file_name_spec.rb
345
+ - spec/unit/mutants/storage_spec.rb
324
346
  - spec/unit/pattern_spec.rb
325
347
  - spec/unit/subjects/environment/type_spec.rb
326
348
  - spec/unit/subjects/environment_spec.rb
327
349
  - spec/unit/subjects/subject_spec.rb
328
- - spec/unit/subjects/test_set_spec.rb
350
+ - spec/unit/tests/test_set_spec.rb