mutiny 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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