diff --git a/circle.yml b/circle.yml index c472653e..82bd0bee 100644 --- a/circle.yml +++ b/circle.yml @@ -1,7 +1,7 @@ --- machine: ruby: - version: 2.0.0 + version: 2.1.2 test: override: - bundle exec rake ci diff --git a/config/flay.yml b/config/flay.yml index cacdaef7..bd95820c 100644 --- a/config/flay.yml +++ b/config/flay.yml @@ -1,3 +1,3 @@ --- threshold: 18 -total_score: 839 +total_score: 812 diff --git a/config/mutant.yml b/config/mutant.yml index 7d1e4e64..b0d051e5 100644 --- a/config/mutant.yml +++ b/config/mutant.yml @@ -10,6 +10,5 @@ ignore_subjects: - Mutant::Reporter* - Mutant::CLI* - Mutant.singleton_subclass_instance -- Mutant.symbolset # Executing this has undefined behavior with the zombifier - Mutant.zombify diff --git a/config/reek.yml b/config/reek.yml index 2bcd8d8d..90157c06 100644 --- a/config/reek.yml +++ b/config/reek.yml @@ -51,9 +51,11 @@ NestedIterators: - Mutant#self.singleton_subclass_instance - Mutant::CLI#parse - Mutant::Mutator::Util::Array::Element#dispatch - - Mutant::Reporter::CLI::Printer::Config::Runner#generic_stats + - Mutant::Reporter::CLI::Report::Config#generic_stats - Mutant::RequireHighjack#infect - Mutant::RequireHighjack#desinfect + - Mutant::Reporter::CLI::Registry#included + - Mutant::Strategy#tests - Parser::Lexer#self.new max_allowed_nesting: 1 ignore_iterators: [] @@ -63,6 +65,7 @@ RepeatedConditional: enabled: true exclude: - Mutant::Mutator + - Mutant::Rspec::Strategy - Mutant::Reporter::CLI max_ifs: 1 TooManyInstanceVariables: @@ -84,12 +87,14 @@ TooManyStatements: enabled: true exclude: - Mutant#self.singleton_subclass_instance + - Mutant#self.isolate - Mutant::Rspec::Killer#run - Mutant::Reporter::CLI#colorized_diff - - Mutant::Reporter::CLI::Printer::Config::Runner#run - - Mutant::Runner#dispatch + - Mutant::Reporter::CLI::Report::Config#run + - Mutant::Runner#visit_collection - Mutant::Zombifier::File#self.find - Mutant::RequireHighjack#infect + - Mutant::Reporter::CLI::Registry#included # How mutant does CLI parsing is shit - Mutant::CLI#parse - Mutant::CLI#initialize diff --git a/config/rubocop.yml b/config/rubocop.yml index a4409d5c..903f6c7d 100644 --- a/config/rubocop.yml +++ b/config/rubocop.yml @@ -99,6 +99,10 @@ IndentationWidth: EmptyLinesAroundBody: Enabled: false +# I test this style for a while +LambdaCall: + Enabled: false + # I like my style more AccessModifierIndentation: Enabled: false diff --git a/lib/mutant-rspec.rb b/lib/mutant-rspec.rb index 278a95cf..67588bbd 100644 --- a/lib/mutant-rspec.rb +++ b/lib/mutant-rspec.rb @@ -1,5 +1,7 @@ # encoding: UTF-8 +require 'rspec' + require 'mutant/rspec' -require 'mutant/rspec/killer' require 'mutant/rspec/strategy' +require 'mutant/rspec/test' diff --git a/lib/mutant.rb b/lib/mutant.rb index 118a5c9f..674e801a 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -37,18 +37,42 @@ module Mutant self end - # Return a frozen set of symbols from string enumerable + IsolationError = Class.new(RuntimeError) + + # Call block in isolation # - # @param [Enumerable] + # This isolation implements the fork strategy. + # Future strategies will probably use a process pool that can + # handle multiple mutation kills, in-isolation at once. # - # @return [Set] + # @return [Object] # # @api private # - def self.symbolset(strings) - strings.map(&:to_sym).to_set.freeze + def self.isolate(&block) + reader, writer = IO.pipe + + pid = fork do + reader.close + writer.write(Marshal.dump(block.call)) + end + + writer.close + + begin + data = Marshal.load(reader.read) + rescue ArgumentError + raise IsolationError, 'Childprocess wrote un-unmarshallable data' + end + + status = Process.waitpid2(pid).last + + unless status.exitstatus.zero? + raise IsolationError, "Childprocess exited with nonzero exit status: #{status.exitstatus}" + end + + data end - private_class_method :symbolset # Define instance of subclassed superclass as constant # @@ -78,6 +102,7 @@ end # Mutant require 'mutant/version' require 'mutant/cache' +require 'mutant/delegator' require 'mutant/node_helpers' require 'mutant/warning_filter' require 'mutant/warning_expectation' @@ -162,27 +187,33 @@ require 'mutant/matcher/scope' require 'mutant/matcher/filter' require 'mutant/matcher/null' require 'mutant/killer' -require 'mutant/killer/static' -require 'mutant/killer/forking' -require 'mutant/killer/forked' +require 'mutant/test' require 'mutant/strategy' require 'mutant/runner' require 'mutant/runner/config' require 'mutant/runner/subject' require 'mutant/runner/mutation' +require 'mutant/runner/killer' require 'mutant/cli' require 'mutant/cli/classifier' require 'mutant/cli/classifier/namespace' require 'mutant/cli/classifier/method' require 'mutant/color' -require 'mutant/differ' +require 'mutant/diff' require 'mutant/reporter' require 'mutant/reporter/null' +require 'mutant/reporter/trace' require 'mutant/reporter/cli' +require 'mutant/reporter/cli/registry' require 'mutant/reporter/cli/printer' -require 'mutant/reporter/cli/printer/config' -require 'mutant/reporter/cli/printer/subject' -require 'mutant/reporter/cli/printer/killer' -require 'mutant/reporter/cli/printer/mutation' +require 'mutant/reporter/cli/report' +require 'mutant/reporter/cli/report/config' +require 'mutant/reporter/cli/report/subject' +require 'mutant/reporter/cli/report/mutation' +require 'mutant/reporter/cli/progress' +require 'mutant/reporter/cli/progress/config' +require 'mutant/reporter/cli/progress/subject' +require 'mutant/reporter/cli/progress/mutation' +require 'mutant/reporter/cli/progress/noop' require 'mutant/zombifier' require 'mutant/zombifier/file' diff --git a/lib/mutant/constants.rb b/lib/mutant/constants.rb index ec34ad02..60b01f38 100644 --- a/lib/mutant/constants.rb +++ b/lib/mutant/constants.rb @@ -2,23 +2,23 @@ module Mutant + symbolset = ->(strings) { strings.map(&:to_sym).to_set.freeze } + # Set of nodes that cannot be on the LHS of an assignment - NOT_ASSIGNABLE = symbolset %w( - int float str dstr class module self - ) + NOT_ASSIGNABLE = symbolset.(%w(int float str dstr class module self)) # Set of op-assign types - OP_ASSIGN = symbolset %w(or_asgn and_asgn op_asgn) + OP_ASSIGN = symbolset.call(%w(or_asgn and_asgn op_asgn)) # Set of node types that are not valid when emitted standalone - NOT_STANDALONE = symbolset %w( splat restarg block_pass) - INDEX_OPERATORS = symbolset %w([] []=) - UNARY_METHOD_OPERATORS = symbolset %w(~@ +@ -@ !) + NOT_STANDALONE = symbolset.(%w( splat restarg block_pass)) + INDEX_OPERATORS = symbolset.(%w([] []=)) + UNARY_METHOD_OPERATORS = symbolset.(%w(~@ +@ -@ !)) # Operators ruby implementeds as methods - METHOD_OPERATORS = symbolset %w( + METHOD_OPERATORS = symbolset.(%w( <=> === []= [] <= >= == !~ != =~ << >> ** * % / | ^ & < > + - ~@ +@ -@ ! - ) + )) BINARY_METHOD_OPERATORS = ( METHOD_OPERATORS - (INDEX_OPERATORS + UNARY_METHOD_OPERATORS) @@ -32,10 +32,10 @@ module Mutant # # not - 1.8 only, mutant does not support 1.8 # - NODE_BLACKLIST = symbolset %w(not) + NODE_BLACKLIST = symbolset.(%w(not)) # Nodes that are NOT generated by parser but used by mutant / unparser. - NODE_EXTRA = symbolset %w(empty) + NODE_EXTRA = symbolset.(%w(empty)) NODE_TYPES = ((Parser::Meta::NODE_TYPES + NODE_EXTRA) - NODE_BLACKLIST).to_set.freeze diff --git a/lib/mutant/delegator.rb b/lib/mutant/delegator.rb new file mode 100644 index 00000000..e7e0e959 --- /dev/null +++ b/lib/mutant/delegator.rb @@ -0,0 +1,49 @@ +module Mutant + module Delegator + module ClassMethods + + private + # Create delegators to object + # + # @return [undefined] + # + # @api private + # + def delegate(*names) + names.each do |name| + define_delegator(name) + end + end + + # Create delegator to object + # + # @param [Symbol] name + # + # @return [undefined] + # + # @api private + # + def define_delegator(name) + define_method(name) do + object.public_send(name) + end + private name + end + + end # ClassMethods + + # Hook called when module is included + # + # @param [Class,Module] host + # + # @api private + # + def self.included(host) + super + host.class_eval do + extend ClassMethods + end + end + + end # Delegator +end # Mutant diff --git a/lib/mutant/differ.rb b/lib/mutant/diff.rb similarity index 92% rename from lib/mutant/differ.rb rename to lib/mutant/diff.rb index 079590f7..d6aedb03 100644 --- a/lib/mutant/differ.rb +++ b/lib/mutant/diff.rb @@ -2,7 +2,7 @@ module Mutant # Class to create diffs from source code - class Differ + class Diff include Adamantium::Flat, Concord.new(:old, :new) # Return source diff @@ -20,7 +20,7 @@ module Mutant when 0 nil when 1 - Diff::LCS::Hunk.new(old, new, diffs.first, max_length, 0) + ::Diff::LCS::Hunk.new(old, new, diffs.first, max_length, 0) .diff(:unified) << "\n" else $stderr.puts( @@ -55,7 +55,7 @@ module Mutant # @param [String] old # @param [String] new # - # @return [Differ] + # @return [Diff] # # @api private # @@ -85,7 +85,7 @@ module Mutant # @api private # def diffs - Diff::LCS.diff(old, new) + ::Diff::LCS.diff(old, new) end memoize :diffs @@ -118,5 +118,5 @@ module Mutant end.format(line) end - end # Differ + end # Diff end # Mutant diff --git a/lib/mutant/killer.rb b/lib/mutant/killer.rb index 3618bfd0..a6dc598f 100644 --- a/lib/mutant/killer.rb +++ b/lib/mutant/killer.rb @@ -1,123 +1,46 @@ # encoding: utf-8 module Mutant - # Abstract base class for mutant killers + # Mutation killer class Killer - include Adamantium::Flat, AbstractType - include Equalizer.new(:strategy, :mutation, :killed?) + include Adamantium::Flat, Anima.new(:test, :mutation) - # Return strategy - # - # @return [Strategy] - # - # @api private - # - attr_reader :strategy + # Report object for kill results + class Report + include Anima.new( + :killer, + :test_report + ) - # Return mutation to kill - # - # @return [Mutation] - # - # @api private - # - attr_reader :mutation - - # Initialize killer object - # - # @param [Strategy] strategy - # @param [Mutation] mutation - # - # @return [undefined] - # - # @api private - # - def initialize(strategy, mutation) - @strategy, @mutation = strategy, mutation - @killed = run - end - - # Test for kill failure - # - # @return [true] - # when killer succeeded - # - # @return [false] - # otherwise - # - # @api private - # - def success? - mutation.success?(self) - end - memoize :success? - - # Test if mutant was killed - # - # @return [true] - # if mutant was killed - # - # @return [false] - # otherwise - # - # @api private - # - def killed? - @killed - end - - # Return mutated source - # - # @return [String] - # - # @api private - # - def mutation_source - mutation.source - end - - private - - # Return subject - # - # @return [Subject] - # - # @api private - # - def subject - mutation.subject - end - - # Run killer - # - # @return [true] - # when mutant was killed - # - # @return [false] - # otherwise - # - # @api private - # - abstract_method :run - - # Null killer that never kills a mutation - class Null < self - - private - - # Run killer + # Test if kill was successful # - # @return [true] - # when mutant was killed - # - # @return [false] - # otherwise + # @return [Boolean] # # @api private # - def run - false + def success? + killer.mutation.should_fail?.equal?(test_report.failed?) end + end # Report + + # Return killer report + # + # @return [Killer::Report] + # + # @api private + # + def run + test_report = Mutant.isolate do + mutation.insert + test.run + end + + Report.new( + killer: self, + test_report: test_report.update(test: test) + ) end + end # Killer end # Mutant diff --git a/lib/mutant/killer/forked.rb b/lib/mutant/killer/forked.rb deleted file mode 100644 index 6495f165..00000000 --- a/lib/mutant/killer/forked.rb +++ /dev/null @@ -1,46 +0,0 @@ -# encoding: utf-8 - -module Mutant - class Killer - - # Killer that executes other killer in forked environment - class Forked < self - - # Initialize object - # - # @param [Killer] killer - # @param [Strategy] strategy - # @param [Mutation] mutation - # - # @api private - # - def initialize(killer, strategy, mutation) - @killer = killer - super(strategy, mutation) - end - - private - - # Run killer - # - # @return [true] - # if mutant was killed - # - # @return [false] - # otherwise - # - # @api private - # - def run - pid = fork do - killer = @killer.new(strategy, mutation) - exit(killer.killed? ? CLI::EXIT_SUCCESS : CLI::EXIT_FAILURE) - end - - status = Process.wait2(pid).last - status.exited? && status.success? - end - - end # Forked - end # Killer -end # Mutant diff --git a/lib/mutant/killer/forking.rb b/lib/mutant/killer/forking.rb deleted file mode 100644 index ae5a6162..00000000 --- a/lib/mutant/killer/forking.rb +++ /dev/null @@ -1,46 +0,0 @@ -# encoding: utf-8 - -module Mutant - class Killer - - # A killer that executes other killer in forked environemnts - class Forking < self - include Equalizer.new(:killer) - - # Return killer - # - # @return [Killer] - # - # @api private - # - attr_reader :killer - - # Initalize killer - # - # @param [Killer] killer - # the killer that will be used - # - # @return [undefined] - # - # @api private - # - def initialize(killer) - @killer = killer - end - - # Return killer instance - # - # @param [Strategy] strategy - # @param [Mutation] mutation - # - # @return [Killer::Forked] - # - # @api private - # - def new(strategy, mutation) - Forked.new(killer, strategy, mutation) - end - - end # Forking - end # Killer -end # Mutant diff --git a/lib/mutant/killer/static.rb b/lib/mutant/killer/static.rb deleted file mode 100644 index 5b09e720..00000000 --- a/lib/mutant/killer/static.rb +++ /dev/null @@ -1,34 +0,0 @@ -# encoding: utf-8 - -module Mutant - class Killer - # Abstract base class for killer with static result - class Static < self - - # Return result - # - # @return [true] - # if mutation was killed - # - # @return [false] - # otherwise - # - # @api private - # - def run - self.class::RESULT - end - - # Killer that is always successful - class Success < self - RESULT = true - end # Success - - # Killer that always fails - class Fail < self - RESULT = false - end # Fail - - end # Static - end # Killer -end # Mutant diff --git a/lib/mutant/mutation.rb b/lib/mutant/mutation.rb index 69f86784..9bcd443b 100644 --- a/lib/mutant/mutation.rb +++ b/lib/mutant/mutation.rb @@ -6,6 +6,9 @@ module Mutant include AbstractType, Adamantium::Flat include Concord::Public.new(:subject, :node) + CODE_DELIMITER = "\0".freeze + CODE_RANGE = (0..4).freeze + # Return mutated root node # # @return [Parser::AST::Node] @@ -54,8 +57,9 @@ module Mutant # @api private # def identification - "#{subject.identification}:#{code}" + "#{self.class::SYMBOL}:#{subject.identification}:#{code}" end + memoize :identification # Return mutation code # @@ -64,7 +68,7 @@ module Mutant # @api private # def code - sha1[0..4] + sha1[CODE_RANGE] end memoize :code @@ -89,6 +93,16 @@ module Mutant subject.source end + # Test if test should fail under mutation + # + # @return [Boolean] + # + # @api private + # + def should_fail? + self.class::SHOULD_FAIL + end + private # Return sha1 sum of source and subject identification @@ -98,7 +112,7 @@ module Mutant # @api private # def sha1 - Digest::SHA1.hexdigest(subject.identification + 0.chr + source) + Digest::SHA1.hexdigest(subject.identification + CODE_DELIMITER + source) end memoize :sha1 diff --git a/lib/mutant/mutation/evil.rb b/lib/mutant/mutation/evil.rb index 0fb19b2c..a4d2b6bf 100644 --- a/lib/mutant/mutation/evil.rb +++ b/lib/mutant/mutation/evil.rb @@ -5,16 +5,8 @@ module Mutant # Evul mutation class Evil < self - # Return identification - # - # @return [String] - # - # @api private - # - def identification - "evil:#{super}" - end - memoize :identification + SHOULD_FAIL = true + SYMBOL = 'evil'.freeze # Test if killer is successful # diff --git a/lib/mutant/mutation/neutral.rb b/lib/mutant/mutation/neutral.rb index 96165f8a..54b0f80c 100644 --- a/lib/mutant/mutation/neutral.rb +++ b/lib/mutant/mutation/neutral.rb @@ -5,41 +5,15 @@ module Mutant # Neutral mutation class Neutral < self - SYMBOL = 'neutral' + SYMBOL = 'neutral'.freeze + SHOULD_FAIL = false # Noop mutation, special case of neutral class Noop < self - SYMBOL = 'noop' + SYMBOL = 'noop'.freeze - end - - # Return identification - # - # @return [String] - # - # @api private - # - def identification - "#{self.class::SYMBOL}:#{super}" - end - memoize :identification - - # Test if killer is successful - # - # @param [Killer] killer - # - # @return [true] - # if killer did NOT killed mutation - # - # @return [false] - # otherwise - # - # @api private - # - def success?(killer) - !killer.killed? - end + end # Noop end # Neutral end # Mutation diff --git a/lib/mutant/node_helpers.rb b/lib/mutant/node_helpers.rb index c1b90e75..411e6d7b 100644 --- a/lib/mutant/node_helpers.rb +++ b/lib/mutant/node_helpers.rb @@ -13,7 +13,7 @@ module Mutant # @api private # def s(type, *children) - ::Parser::AST::Node.new(type, children) + Parser::AST::Node.new(type, children) end module_function :s @@ -21,8 +21,6 @@ module Mutant s(:send, s(:float, 0.0), :/, s(:float, 0.0)) INFINITY = s(:send, s(:float, 1.0), :/, s(:float, 0.0)) - NEW_OBJECT = - s(:send, s(:const, s(:cbase), :Object), :new) NEGATIVE_INFINITY = s(:send, s(:float, -1.0), :/, s(:float, 0.0)) diff --git a/lib/mutant/reporter.rb b/lib/mutant/reporter.rb index 7572ed8d..6cff250a 100644 --- a/lib/mutant/reporter.rb +++ b/lib/mutant/reporter.rb @@ -15,5 +15,15 @@ module Mutant # abstract_method :report + # Report progress on object + # + # @param [Object] object + # + # @return [self] + # + # @api private + # + abstract_method :progress + end # Reporter end # Mutant diff --git a/lib/mutant/reporter/cli.rb b/lib/mutant/reporter/cli.rb index f6a0f8bd..d171c1f8 100644 --- a/lib/mutant/reporter/cli.rb +++ b/lib/mutant/reporter/cli.rb @@ -4,10 +4,23 @@ module Mutant class Reporter # Reporter that reports in human readable format class CLI < self - include Concord::Public.new(:output) + include Concord.new(:output) NL = "\n".freeze + # Report progress object + # + # @param [Object] object + # + # @return [self] + # + # @api private + # + def progress(object) + Progress.run(output, object) + self + end + # Report object # # @param [Object] object @@ -17,7 +30,7 @@ module Mutant # @api private # def report(object) - Printer.visit(object, output) + Report.run(output, object) self end diff --git a/lib/mutant/reporter/cli/printer.rb b/lib/mutant/reporter/cli/printer.rb index 077c02bb..85dff4c4 100644 --- a/lib/mutant/reporter/cli/printer.rb +++ b/lib/mutant/reporter/cli/printer.rb @@ -6,69 +6,19 @@ module Mutant # CLI runner status printer base class class Printer - include AbstractType, Adamantium::Flat, Concord.new(:object, :output) + include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object) - REGISTRY = {} - - # Create delegators to object + # Run printer on object to output # - # @return [undefined] + # @param [IO] output + # @param [Object] object # - # @api private + # @return [self] # - def self.delegate(*names) - names.each do |name| - define_delegator(name) - end - end - private_class_method :delegate - - # Create delegator to object - # - # @param [Symbol] name - # - # @return [undefined] - # - # @api private - # - def self.define_delegator(name) - define_method(name) do - object.public_send(name) - end - private name - end - private_class_method :define_delegator - - # Registre handler for class - # - # @param [Class] klass - # - # @return [undefined] - # - # @api private - # - def self.handle(klass) - REGISTRY[klass] = self - end - - # Finalize CLI reporter - # - # @return [undefined] - # - # @api private - # - def self.finalize - REGISTRY.freeze - end - - # Build printer - # - # @return [Printer] - # - # @api private - # - def self.build(*args) - new(*args) + def self.run(output, object) + handler = lookup(object.class) + handler.new(output, object).run + self end # Run printer @@ -77,49 +27,6 @@ module Mutant # # @api private # - def self.run(*args) - build(*args).run - self - end - - # Visit object - # - # @param [Object] object - # @param [IO] output - # - # @return [undefined] - # - # @api private - # - def self.visit(object, output) - printer = lookup(object.class) - printer.run(object, output) - end - - # Lookup printer class - # - # @param [Class] klass - # - # @return [Class:Printer] - # if found - # - # @raise [RuntimeError] - # otherwise - # - # @api private - # - def self.lookup(klass) - current = klass - until current == Object - if REGISTRY.key?(current) - return REGISTRY.fetch(current) - end - current = current.superclass - end - raise "No printer for: #{klass}" - end - private_class_method :lookup - abstract_method :run private @@ -130,7 +37,7 @@ module Mutant # # @api private # - def color + def status_color success? ? Color::GREEN : Color::RED end @@ -143,7 +50,7 @@ module Mutant # @api private # def visit(object) - self.class.visit(object, output) + self.class.run(output, object) end # Print an info line to output @@ -163,7 +70,7 @@ module Mutant # @api private # def status(string, *arguments) - puts(colorize(color, sprintf(string, *arguments))) + puts(colorize(status_color, sprintf(string, *arguments))) end # Print a line to output diff --git a/lib/mutant/reporter/cli/printer/config.rb b/lib/mutant/reporter/cli/printer/config.rb deleted file mode 100644 index fab9b0d5..00000000 --- a/lib/mutant/reporter/cli/printer/config.rb +++ /dev/null @@ -1,154 +0,0 @@ -# encoding: utf-8 - -module Mutant - class Reporter - class CLI - class Printer - - # Printer for configuration - class Config < self - - handle(Mutant::Config) - - delegate :matcher, :strategy, :expected_coverage - - # Report configuration - # - # @param [Mutant::Config] config - # - # @return [self] - # - # @api private - # - def run - info 'Mutant configuration:' - info 'Matcher: %s', matcher.inspect - info 'Strategy: %s', strategy.inspect - info 'Expect Coverage: %02f%%', expected_coverage.inspect - self - end - - # Config results printer - class Runner < self - - handle(Mutant::Runner::Config) - - delegate( - :amount_kills, :amount_mutations, :amount_kils, - :coverage, :subjects, :failed_subjects, :runtime, :mutations - ) - - # Run printer - # - # @return [self] - # - # @api private - # - def run - print_mutations - info 'Subjects: %s', amount_subjects - info 'Mutations: %s', amount_mutations - info 'Kills: %s', amount_kills - info 'Alive: %s', amount_alive - info 'Runtime: %0.2fs', runtime - info 'Killtime: %0.2fs', killtime - info 'Overhead: %0.2f%%', overhead - status 'Coverage: %0.2f%%', coverage - status 'Expected: %0.2f%%', object.config.expected_coverage - print_generic_stats - self - end - - private - - # Print generic stats - # - # @return [undefined] - # - # @api private - # - def print_generic_stats - stats = generic_stats.to_a.sort_by(&:last) - return if stats.empty? - info('Nodes handled by generic mutator (type:occurrences):') - stats.reverse_each do |type, amount| - info('%-10s: %d', type, amount) - end - end - - # Return stats for nodes handled by generic mutator - # - # @return [Hash] - # - # @api private - # - def generic_stats - subjects.each_with_object(Hash.new(0)) do |runner, stats| - Walker.run(runner.subject.node) do |node| - if Mutator::Registry.lookup(node) == Mutator::Node::Generic - stats[node.type] += 1 - end - end - end - end - - # Return amount of subjects - # - # @return [Fixnum] - # - # @api private - # - def amount_subjects - subjects.length - end - - # Print mutations - # - # @return [undefined] - # - # @api private - # - def print_mutations - failed_subjects.each do |subject| - Subject::Runner::Details.run(subject, output) - end - end - - # Return amount of time in killers - # - # @return [Float] - # - # @api private - # - def killtime - mutations.map(&:runtime).inject(0, :+) - end - memoize :killtime - - # Return mutant overhead - # - # @return [Float] - # - # @api private - # - def overhead - return 0 if runtime.zero? - Rational(runtime - killtime, runtime) * 100 - end - - # Return amount of alive mutations - # - # @return [Fixnum] - # - # @api private - # - def amount_alive - object.amount_mutations - amount_kills - end - - end # Runner - end # Config - end # Printer - end # CLI - end # Reporter -end # Mutant diff --git a/lib/mutant/reporter/cli/printer/mutation.rb b/lib/mutant/reporter/cli/printer/mutation.rb deleted file mode 100644 index d1a9059b..00000000 --- a/lib/mutant/reporter/cli/printer/mutation.rb +++ /dev/null @@ -1,103 +0,0 @@ -# encoding: utf-8 - -module Mutant - class Reporter - class CLI - class Printer - # Mutation printer - class Mutation < self - - handle(Runner::Mutation) - - # Build printer - # - # @param [Runner::Mutation] runner - # @param [IO] output - # - # @return [Printer::Mutation] - # - # @api private - # - def self.build(runner, output) - mutation = runner.mutation - lookup(mutation.class).new(runner, output) - end - - # Run mutation printer - # - # @return [undefined] - # - # @api private - # - def run - status('%s', mutation.identification) - puts(details) - end - - private - - # Return mutation - # - # @return [Mutation] - # - # @api private - # - def mutation - object.mutation - end - - # Reporter for noop mutations - class Noop < self - - handle(Mutant::Mutation::Neutral::Noop) - - MESSAGE = [ - 'Parsed subject AST:', - '%s', - 'Unparsed source:', - '%s' - ].join("\n") - - private - - # Return details - # - # @return [String] - # - # @api private - # - def details - sprintf( - MESSAGE, - mutation.subject.node.inspect, - mutation.original_source - ) - end - - end # Noop - - # Reporter for neutral and evil mutations - class Diff < self - - handle(Mutant::Mutation::Neutral) - handle(Mutant::Mutation::Evil) - - # Return diff - # - # @return [String] - # - # @api private - # - def details - original, current = mutation.original_source, mutation.source - differ = Differ.build(original, current) - color? ? differ.colorized_diff : differ.diff - end - - end # Evil - - end # Mutantion - end # Printer - end # CLI - end # Reporter -end # Mutant diff --git a/lib/mutant/reporter/cli/printer/subject.rb b/lib/mutant/reporter/cli/printer/subject.rb deleted file mode 100644 index 873d7ebb..00000000 --- a/lib/mutant/reporter/cli/printer/subject.rb +++ /dev/null @@ -1,150 +0,0 @@ -# encoding: utf-8 - -module Mutant - class Reporter - class CLI - class Printer - - # Subject results printer - class Subject < self - - handle(Mutant::Subject) - - # Run subject results printer - # - # @return [undefined] - # - # @api private - # - def run - info('%s', object.identification) - end - - # Printer for subject runners - class Runner < self - - handle(Mutant::Runner::Subject) - - # Run printer - # - # @return [undefined] - # - # @api private - # - def run - print_progress_bar_finish - print_stats - self - end - - private - - # Return mutation time on subject - # - # @return [Float] - # - # @api private - # - def time - mutations.map(&:runtime).inject(0, :+) - end - - # Return subject - # - # @return [Subject] - # - # @api private - # - def subject - object.subject - end - - FORMAT = '(%02d/%02d) %3d%% - %0.02fs'.freeze - - # Print stats - # - # @return [undefned - # - # @api private - # - def print_stats - status(FORMAT, amount_kills, amount_mutations, coverage, time) - end - - # Print progress bar finish - # - # @return [undefined] - # - # @api private - # - def print_progress_bar_finish - puts unless amount_mutations.zero? - end - - # Return kills - # - # @return [Fixnum] - # - # @api private - # - def amount_kills - fails = object.failed_mutations - fails = fails.length - amount_mutations - fails - end - - # Return amount of mutations - # - # @return [Array] - # - # @api private - # - def amount_mutations - mutations.length - end - - # Return mutations - # - # @return [Array] - # - # @api private - # - def mutations - object.mutations - end - - # Return suject coverage - # - # @return [Float] - # - # @api private - # - def coverage - return 0 if amount_mutations.zero? - Rational(amount_kills, amount_mutations) * 100 - end - - # Detailed subject printer - class Details < self - - # Run subject details printer - # - # @return [undefined] - # - # @api private - # - def run - puts(subject.identification) - object.failed_mutations.each do |mutation| - visit(mutation) - end - print_stats - end - - end # Details - end # Runner - end # Subject - end # Printer - end # CLI - end # Reporter -end # Mutant diff --git a/lib/mutant/reporter/cli/progress.rb b/lib/mutant/reporter/cli/progress.rb new file mode 100644 index 00000000..40b72928 --- /dev/null +++ b/lib/mutant/reporter/cli/progress.rb @@ -0,0 +1,12 @@ +# encoding: utf-8 + +module Mutant + class Reporter + class CLI + # Abstract base class for process printers + class Progress < Printer + include AbstractType, Registry.new + end # Progress + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/cli/progress/config.rb b/lib/mutant/reporter/cli/progress/config.rb new file mode 100644 index 00000000..8d7cf8f8 --- /dev/null +++ b/lib/mutant/reporter/cli/progress/config.rb @@ -0,0 +1,32 @@ +module Mutant + class Reporter + class CLI + class Progress + # Progress printer for configuration + class Config < self + + handle(Mutant::Config) + + delegate :matcher, :strategy, :expected_coverage + + # Report configuration + # + # @param [Mutant::Config] config + # + # @return [self] + # + # @api private + # + def run + info 'Mutant configuration:' + info 'Matcher: %s', matcher.inspect + info 'Strategy: %s', strategy.inspect + info 'Expect Coverage: %02f%%', expected_coverage.inspect + self + end + + end # Progress + end # Printer + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/cli/printer/killer.rb b/lib/mutant/reporter/cli/progress/mutation.rb similarity index 55% rename from lib/mutant/reporter/cli/printer/killer.rb rename to lib/mutant/reporter/cli/progress/mutation.rb index 186e6084..74831dd2 100644 --- a/lib/mutant/reporter/cli/printer/killer.rb +++ b/lib/mutant/reporter/cli/progress/mutation.rb @@ -1,14 +1,12 @@ -# encoding: utf-8 - module Mutant class Reporter class CLI - class Printer + class Progress - # Printer for killer results - class Killer < self + # Mutation progress reporter + class Mutation < self - handle(Mutant::Killer) + handle(Runner::Mutation) SUCCESS = '.'.freeze FAILURE = 'F'.freeze @@ -20,11 +18,7 @@ module Mutant # @api private # def run - if success? - char(SUCCESS, Color::GREEN) - else - char(FAILURE, Color::RED) - end + char(success? ? SUCCESS : FAILURE) end private @@ -32,19 +26,18 @@ module Mutant # Write colorized char # # @param [String] char - # @param [Color] # # @return [undefined] # # @api private # - def char(char, color) - output.write(colorize(color, char)) + def char(char) + output.write(colorize(status_color, char)) output.flush end - end # Killer - end # Printer + end # Mutation + end # Progress end # CLI end # Reporter end # Mutant diff --git a/lib/mutant/reporter/cli/progress/noop.rb b/lib/mutant/reporter/cli/progress/noop.rb new file mode 100644 index 00000000..6937c788 --- /dev/null +++ b/lib/mutant/reporter/cli/progress/noop.rb @@ -0,0 +1,22 @@ +module Mutant + class Reporter + class CLI + class Progress + # Noop CLI progress reporter + class Noop < self + + handle(Mutant::Mutation) + + # Noop progress report + # + # @return [self] + # + def run + self + end + + end # Noop + end # Progress + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/cli/progress/subject.rb b/lib/mutant/reporter/cli/progress/subject.rb new file mode 100644 index 00000000..489a62b1 --- /dev/null +++ b/lib/mutant/reporter/cli/progress/subject.rb @@ -0,0 +1,118 @@ +module Mutant + class Reporter + class CLI + class Progress + # Subject results printer + class Subject < self + + handle(Mutant::Subject) + + # Run subject results printer + # + # @return [undefined] + # + # @api private + # + def run + puts(object.identification) + end + + end # Subject + + # Reporter for subject runners + class SubjectRunner < self + + FORMAT = '(%02d/%02d) %3d%% - %0.02fs'.freeze + + handle(Mutant::Runner::Subject) + + # Run printer + # + # @return [undefined] + # + # @api private + # + def run + print_progress_bar_finish + print_stats + self + end + + private + + # Return mutation time on subject + # + # @return [Float] + # + # @api private + # + def time + mutations.map(&:runtime).inject(0, :+) + end + + # Print stats + # + # @return [undefined] + # + # @api private + # + def print_stats + status(FORMAT, amount_kills, amount_mutations, coverage, time) + end + + # Print progress bar finish + # + # @return [undefined] + # + # @api private + # + def print_progress_bar_finish + puts unless amount_mutations.zero? + end + + # Return kills + # + # @return [Fixnum] + # + # @api private + # + def amount_kills + amount_mutations - object.failed_mutations.length + end + + # Return amount of mutations + # + # @return [Array] + # + # @api private + # + def amount_mutations + mutations.length + end + + # Return mutations + # + # @return [Array] + # + # @api private + # + def mutations + object.mutations + end + + # Return subject coverage + # + # @return [Float] + # + # @api private + # + def coverage + return 0 if amount_mutations.zero? + Rational(amount_kills, amount_mutations) * 100 + end + + end # Runner + end # Progress + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/cli/registry.rb b/lib/mutant/reporter/cli/registry.rb new file mode 100644 index 00000000..086c6d1c --- /dev/null +++ b/lib/mutant/reporter/cli/registry.rb @@ -0,0 +1,77 @@ +module Mutant + class Reporter + class CLI + # Mixin to generate registry semantics + class Registry < Module + include Concord.new(:registry) + + # Return new registry + # + # @return [Registry] + # + # @api private + # + def self.new + super({}) + end + + # Register handler for class + # + # @param [Class] klass + # + # @return [self] + # + # @api private + # + def handle(subject, handler) + raise "Duplicate registration of #{subject}" if registry.key?(subject) + registry[subject] = handler + self + end + + # Lookup handler + # + # @param [Class] subject + # + # @return [Object] + # if found + # + # @raise [RuntimeError] + # otherwise + # + # @api private + # + def lookup(subject) + current = subject + until current == Object + if registry.key?(current) + return registry.fetch(current) + end + current = current.superclass + end + raise "No printer for: #{subject}" + end + + # Hook called when module is included + # + # @param [Class,Module] host + # + # @return [undefined] + # + def included(host) + object = self + host.class_eval do + define_singleton_method(:lookup, &object.method(:lookup)) + private_class_method :lookup + + define_singleton_method(:handle) do |subject| + object.handle(subject, self) + end + private_class_method :handle + end + end + + end # Registry + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/cli/report.rb b/lib/mutant/reporter/cli/report.rb new file mode 100644 index 00000000..0943c894 --- /dev/null +++ b/lib/mutant/reporter/cli/report.rb @@ -0,0 +1,12 @@ +# encoding: utf-8 + +module Mutant + class Reporter + class CLI + # Abstract base class for process printers + class Report < Printer + include AbstractType, Registry.new + end # Report + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/cli/report/config.rb b/lib/mutant/reporter/cli/report/config.rb new file mode 100644 index 00000000..dc5a6c44 --- /dev/null +++ b/lib/mutant/reporter/cli/report/config.rb @@ -0,0 +1,118 @@ +# encoding: utf-8 + +module Mutant + class Reporter + class CLI + class Report + + # Printer for configuration + class Config < self + + handle(Mutant::Runner::Config) + + delegate( + :amount_kills, :amount_mutations, :amount_kils, + :coverage, :subjects, :failed_subjects, :runtime, :mutations + ) + + # Run printer + # + # @return [self] + # + # @api private + # + def run + failed_subjects.each(&method(:visit)) + info 'Subjects: %s', amount_subjects + info 'Mutations: %s', amount_mutations + info 'Kills: %s', amount_kills + info 'Alive: %s', amount_alive + info 'Runtime: %0.2fs', runtime + info 'Killtime: %0.2fs', killtime + info 'Overhead: %0.2f%%', overhead + status 'Coverage: %0.2f%%', coverage + status 'Expected: %0.2f%%', object.config.expected_coverage + print_generic_stats + self + end + + private + + # Print generic stats + # + # @return [undefined] + # + # @api private + # + def print_generic_stats + stats = generic_stats.to_a.sort_by(&:last) + return if stats.empty? + info('Nodes handled by generic mutator (type:occurrences):') + stats.reverse_each do |type, amount| + info('%-10s: %d', type, amount) + end + end + + # Return stats for nodes handled by generic mutator + # + # @return [Hash] + # + # @api private + # + def generic_stats + subjects.each_with_object(Hash.new(0)) do |runner, stats| + Walker.run(runner.subject.node) do |node| + if Mutator::Registry.lookup(node) == Mutator::Node::Generic + stats[node.type] += 1 + end + end + end + end + + # Return amount of subjects + # + # @return [Fixnum] + # + # @api private + # + def amount_subjects + subjects.length + end + + # Return amount of time in killers + # + # @return [Float] + # + # @api private + # + def killtime + mutations.map(&:runtime).inject(0, :+) + end + memoize :killtime + + # Return mutant overhead + # + # @return [Float] + # + # @api private + # + def overhead + return 0 if runtime.zero? + Rational(runtime - killtime, runtime) * 100 + end + + # Return amount of alive mutations + # + # @return [Fixnum] + # + # @api private + # + def amount_alive + object.amount_mutations - amount_kills + end + + end # Config + end # Report + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/cli/report/mutation.rb b/lib/mutant/reporter/cli/report/mutation.rb new file mode 100644 index 00000000..e98ddd56 --- /dev/null +++ b/lib/mutant/reporter/cli/report/mutation.rb @@ -0,0 +1,88 @@ +# encoding: utf-8 + +module Mutant + class Reporter + class CLI + class Report + + # Reporter for mutations + class Mutation < self + + # Run report printer + # + # @return [self] + # + # @api private + # + def run + puts(object.identification) + puts(details) + self + end + + # Reporter for noop mutations + class Noop < self + handle(Mutant::Mutation::Neutral::Noop) + + MESSAGE = [ + 'Parsed subject AST:', + '%s', + 'Unparsed source:', + '%s' + ].join("\n").freeze + + private + + # Return details + # + # @return [self] + # + # @api private + # + def details + MESSAGE % [object.subject.node.inspect, object.original_source] + end + + end # Noop + + # Reporter for mutations producing a diff + class Diff < self + handle(Mutant::Mutation::Evil) + handle(Mutant::Mutation::Neutral) + + private + + # Run report printer + # + # @return [self] + # + # @api private + # + def details + original, current = object.original_source, object.source + diff = Mutant::Diff.build(original, current) + color? ? diff.colorized_diff : diff.diff + end + + end # Diff + end # Mutation + + # Subject report printer + class MutationRunner < self + handle(Mutant::Runner::Mutation) + + # Run report printer + # + # @return [self] + # + # @api private + # + def run + visit(object.mutation) + end + + end # Mutation + end # Report + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/cli/report/subject.rb b/lib/mutant/reporter/cli/report/subject.rb new file mode 100644 index 00000000..899fd294 --- /dev/null +++ b/lib/mutant/reporter/cli/report/subject.rb @@ -0,0 +1,33 @@ +# encoding: utf-8 + +module Mutant + class Reporter + class CLI + class Report + + # Subject report printer + class Subject < self + handle(Mutant::Runner::Subject) + + delegate :subject, :failed_mutations + + # Run report printer + # + # @return [self] + # + # @api private + # + def run + status(subject.identification) + object.tests.each do |test| + puts("- #{test.identification}") + end + object.failed_mutations.each(&method(:visit)) + self + end + + end # Subject + end # Report + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/null.rb b/lib/mutant/reporter/null.rb index 40265d01..e6ff3cbf 100644 --- a/lib/mutant/reporter/null.rb +++ b/lib/mutant/reporter/null.rb @@ -5,6 +5,7 @@ module Mutant # Null reporter class Null < self + include Equalizer.new # Report object # @@ -18,6 +19,18 @@ module Mutant self end + # Report progress on object + # + # @param [Object] _object + # + # @return [self] + # + # @api private + # + def progress(_object) + self + end + end # Null end # Reporter end # Mutant diff --git a/lib/mutant/reporter/trace.rb b/lib/mutant/reporter/trace.rb new file mode 100644 index 00000000..01be1b5c --- /dev/null +++ b/lib/mutant/reporter/trace.rb @@ -0,0 +1,41 @@ +module Mutant + class Reporter + # Reporter to trace report calls, used as a spec adapter + class Trace + include Concord::Public.new(:progress_calls, :report_calls) + + # Return new trace reporter + # + # @return [Tracer] + # + # @api private + # + def self.new + super([], []) + end + + # Report object + # + # @param [Object] object + # + # @return [self] + # + def report(object) + report_calls << object + self + end + + # Report new progress on object + # + # @param [Object] object + # + # @return [self] + # + def progress(object) + progress_calls << object + self + end + + end # Tracker + end # reporter +end # Mutant diff --git a/lib/mutant/rspec.rb b/lib/mutant/rspec.rb index 1229b292..45b9e8cf 100644 --- a/lib/mutant/rspec.rb +++ b/lib/mutant/rspec.rb @@ -1,7 +1,5 @@ # encoding: UTF-8 -require 'rspec' - module Mutant # Rspec integration namespace module Rspec diff --git a/lib/mutant/rspec/killer.rb b/lib/mutant/rspec/killer.rb deleted file mode 100644 index 9d280f08..00000000 --- a/lib/mutant/rspec/killer.rb +++ /dev/null @@ -1,104 +0,0 @@ -# encoding: utf-8 - -module Mutant - module Rspec - # Runner for rspec tests - class Killer < Mutant::Killer - - private - - # Run rspec test - # - # @return [true] - # when test is NOT successful - # - # @return [false] - # otherwise - # - # @api private - # - def run - mutation.insert - - if example_groups.nil? || example_groups.empty? - $stderr.puts("No rspec example groups found for: #{match_prefixes.join(', ')}") - return false - end - - example_groups.each do |group| - return true unless group.run(reporter) - end - - false - end - - # Return match prefixes - # - # @return [Enumerble] - # - # @api private - # - def match_prefixes - subject.match_prefixes - end - - # Return example groups - # - # @return [Array] - # - # @api private - # - def example_groups - match_prefixes.each do |match_expression| - example_groups = find_with(match_expression) - return example_groups unless example_groups.empty? - end - - nil - end - memoize :example_groups - - # Return example groups that match expression - # - # @param [String] match_expression - # - # @return [Enumerable] - # - # @api private - # - def find_with(match_expression) - all_example_groups.select do |example_group| - example_group.description.start_with?(match_expression) - end - end - - # Return all example groups - # - # @return [Enumerable] - # - # @api private - # - def all_example_groups - strategy.example_groups - end - - # Choose and memoize RSpec reporter - # - # @return [RSpec::Core::Reporter] - # - # @api private - # - def reporter - reporter_class = RSpec::Core::Reporter - - if strategy.rspec2? - reporter_class.new - else - reporter_class.new(strategy.configuration) - end - end - memoize :reporter, freezer: :noop - - end # Killer - end # Rspec -end # Mutant diff --git a/lib/mutant/rspec/strategy.rb b/lib/mutant/rspec/strategy.rb index 701d4533..4414308f 100644 --- a/lib/mutant/rspec/strategy.rb +++ b/lib/mutant/rspec/strategy.rb @@ -2,13 +2,12 @@ module Mutant module Rspec + # Rspec killer strategy class Strategy < Mutant::Strategy register 'rspec' - KILLER = Killer::Forking.new(Rspec::Killer) - # Setup rspec strategy # # @return [self] @@ -25,6 +24,34 @@ module Mutant end memoize :setup + # Return new reporter + # + # @api private + # + def reporter + reporter_class = RSpec::Core::Reporter + + if rspec2? + reporter_class.new + else + reporter_class.new(configuration) + end + end + + # Detect RSpec 2 + # + # @return [true] + # when RSpec 2 + # + # @return [false] + # otherwise + # + # @api private + # + def rspec2? + RSpec::Core::Version::STRING.start_with?('2.') + end + # Return configuration # # @return [RSpec::Core::Configuration] @@ -46,20 +73,6 @@ module Mutant world.example_groups end - # Detect RSpec 2 - # - # @return [true] - # when RSpec 2 - # - # @return [false] - # otherwise - # - # @api private - # - def rspec2? - RSpec::Core::Version::STRING.start_with?('2.') - end - private # Return world @@ -73,6 +86,18 @@ module Mutant end memoize :world, freezer: :noop + # Return all available tests + # + # @return [Enumerable] + # + # @api private + # + def all_tests + example_groups.map do |example_group| + Test.new(self, example_group) + end + end + # Return options # # @return [RSpec::Core::ConfigurationOptions] diff --git a/lib/mutant/rspec/test.rb b/lib/mutant/rspec/test.rb new file mode 100644 index 00000000..750770a2 --- /dev/null +++ b/lib/mutant/rspec/test.rb @@ -0,0 +1,37 @@ +module Mutant + module Rspec + # Rspec test abstraction + class Test < Mutant::Test + include Concord.new(:strategy, :example_group) + + PREFIX = :rspec + + # Return subject identification + # + # @return [String] + # + # @api private + # + def subject_identification + example_group.description + end + memoize :subject_identification + + # Run test, return report + # + # @return [String] + # + # @api private + # + def run + flag = example_group.run(strategy.reporter) + Report.new( + test: self, + output: '', + success: flag + ) + end + + end # Test + end # Rspec +end # Mutant diff --git a/lib/mutant/runner.rb b/lib/mutant/runner.rb index 5f0adc6b..9020d061 100644 --- a/lib/mutant/runner.rb +++ b/lib/mutant/runner.rb @@ -48,9 +48,9 @@ module Mutant # # @api private # - def self.run(config, object) + def self.run(config, object, *arguments) handler = lookup(object.class) - handler.new(config, object) + handler.new(config, object, *arguments) end # Return config @@ -101,16 +101,6 @@ module Mutant (@end || Time.now) - @start end - # Return reporter - # - # @return [Reporter] - # - # @api private - # - def reporter - config.reporter - end - # Test if runner is successful # # @return [true] @@ -133,7 +123,7 @@ module Mutant # abstract_method :run - # Return reporter + # Run reporter on object # # @param [Object] object # @@ -141,20 +131,32 @@ module Mutant # # @api private # - def report(object) - reporter.report(object) + def progress(object) + reporter.progress(object) end - # Perform dispatch + # Return reporter + # + # @return [Reporter] + # + # @api private + # + def reporter + config.reporter + end + + # Perform dispatch on multiple inputs + # + # @param [Enumerable] input # # @return [Enumerable] # # @api private # - def dispatch(input) + def visit_collection(input, *arguments) collection = [] input.each do |object| - runner = visit(object) + runner = visit(object, *arguments) collection << runner @stop = runner.stop? break if @stop @@ -170,8 +172,8 @@ module Mutant # # @api private # - def visit(object) - Runner.run(config, object) + def visit(object, *arguments) + Runner.run(config, object, *arguments) end end # Runner diff --git a/lib/mutant/runner/config.rb b/lib/mutant/runner/config.rb index 5497d8f8..69431ac1 100644 --- a/lib/mutant/runner/config.rb +++ b/lib/mutant/runner/config.rb @@ -5,6 +5,9 @@ module Mutant # Runner for object config class Config < self + # The expected coverage precision + COVERAGE_PRECISION = 1 + register Mutant::Config # Run runner for object @@ -40,8 +43,6 @@ module Mutant end memoize :failed_subjects - COVERAGE_PRECISION = 1 - # Test if run was successful # # @return [true] @@ -122,7 +123,7 @@ module Mutant def run_subjects strategy = self.strategy strategy.setup - @subjects = dispatch(config.subjects) + @subjects = visit_collection(config.subjects) strategy.teardown end @@ -133,10 +134,10 @@ module Mutant # @api private # def run - report(config) + progress(config) run_subjects @end = Time.now - report(self) + reporter.report(self) end end # Config diff --git a/lib/mutant/runner/killer.rb b/lib/mutant/runner/killer.rb new file mode 100644 index 00000000..48f11012 --- /dev/null +++ b/lib/mutant/runner/killer.rb @@ -0,0 +1,51 @@ +module Mutant + class Runner + # Killer runner + class Killer < self + include Equalizer.new(:config, :killer) + + register Mutant::Killer + + # Return killer + # + # @return [Killer] + # + # @api private + # + attr_reader :killer + protected :killer + + # Test if killer ran successfully + # + # @return [Boolean] + # + # @api private + def success? + @report.success? + end + + # Initialize object + # + # @param [Config] config + # @param [Mutation] mutation + # + # @return [undefined] + # + # @api private + # + def initialize(config, killer) + @killer = killer + super(config) + end + + # Run killer + # + # @api private + # + def run + @report = killer.run + end + + end # Killer + end # Runner +end # Mutant diff --git a/lib/mutant/runner/mutation.rb b/lib/mutant/runner/mutation.rb index c88e508d..415a781a 100644 --- a/lib/mutant/runner/mutation.rb +++ b/lib/mutant/runner/mutation.rb @@ -4,7 +4,7 @@ module Mutant class Runner # Mutation runner class Mutation < self - include Equalizer.new(:config, :mutation) + include Equalizer.new(:config, :mutation, :tests) register Mutant::Mutation @@ -16,26 +16,28 @@ module Mutant # attr_reader :mutation - # Return killer instance + # Return killers # # @return [Killer] # # @api private # - attr_reader :killer + attr_reader :killers # Initialize object # # @param [Config] config # @param [Mutation] mutation + # @param [Enumerable] tests # # @return [undefined] # # @api private # - def initialize(config, mutation) - @mutation = mutation + def initialize(config, mutation, tests) + @mutation, @tests = mutation, tests super(config) + @stop = config.fail_fast && !success? end # Test if mutation was handeled successfully @@ -49,7 +51,7 @@ module Mutant # @api private # def success? - mutation.success?(killer) + killers.any?(&:success?) end private @@ -61,9 +63,14 @@ module Mutant # @api private # def run - @killer = config.strategy.kill(mutation) - report(killer) - @stop = config.fail_fast && !killer.success? + progress(mutation) + @killers = @tests.map do |test| + Mutant::Killer.new( + mutation: mutation, + test: test + ) + end.map(&method(:visit)) + progress(self) end end # Mutation diff --git a/lib/mutant/runner/subject.rb b/lib/mutant/runner/subject.rb index 160ef0c9..0aea485e 100644 --- a/lib/mutant/runner/subject.rb +++ b/lib/mutant/runner/subject.rb @@ -63,6 +63,17 @@ module Mutant failed_mutations.empty? end + # Return tests used to kill mutations on this subject + # + # @return [Enumerable] + # + # @api private + # + def tests + config.strategy.tests(subject) + end + memoize :tests + private # Perform operation @@ -72,10 +83,9 @@ module Mutant # @api private # def run - subject = self.subject - report(subject) - @mutations = dispatch(subject.mutations) - report(self) + progress(subject) + @mutations = visit_collection(subject.mutations, tests) + progress(self) end end # Subject diff --git a/lib/mutant/strategy.rb b/lib/mutant/strategy.rb index dc7ba602..be177c6d 100644 --- a/lib/mutant/strategy.rb +++ b/lib/mutant/strategy.rb @@ -54,39 +54,53 @@ module Mutant self end - # Kill mutation + # Return all available tests by strategy # - # @param [Mutation] mutation - # - # @return [Killer] + # @return [Enumerable] # # @api private # - def kill(mutation) - killer.new(self, mutation) + abstract_method :all_tests + + # Return tests for mutation + # + # TODO: This logic is now centralized but still fucked. + # + # @param [Mutation] mutation + # + # @return [Enumerable] + # + # @api private + # + def tests(subject) + subject.match_prefixes.map do |match_expression| + tests = all_tests.select do |test| + test.subject_identification.start_with?(match_expression) + end + return tests if tests.any? + end + + [] end private - # Return killer - # - # @return [Class:Killer] - # - # @api private - # - def killer - self.class::KILLER - end - # Null strategy that never kills a mutation class Null < self - register 'null' + register('null') - KILLER = Killer::Null + # Return all tests + # + # @return [Enumerable] + # + # @api private + # + def all_tests + EMPTY_ARRAY + end end # Null end # Strategy - end # Mutant diff --git a/lib/mutant/subject/method/instance.rb b/lib/mutant/subject/method/instance.rb index 8b6bdd6a..9d9826e6 100644 --- a/lib/mutant/subject/method/instance.rb +++ b/lib/mutant/subject/method/instance.rb @@ -68,7 +68,7 @@ module Mutant # def prepare scope.send(:memoized_methods).instance_variable_get(:@memory).delete(name) - scope.send(:undef_method, name) + super self end diff --git a/lib/mutant/test.rb b/lib/mutant/test.rb new file mode 100644 index 00000000..eb568029 --- /dev/null +++ b/lib/mutant/test.rb @@ -0,0 +1,86 @@ +module Mutant + # Abstract base class for test that might kill a mutation + class Test + include AbstractType, Adamantium::Flat + + # Object to report test status + class Report + include Adamantium::Flat, Anima::Update, Anima.new( + :test, + :output, + :success + ) + + alias_method :success?, :success + + # Test if test failed + # + # @return [Boolean] + # + # @api private + # + def failed? + !success? + end + + # Return marshallable data + # + # NOTE: + # + # The test is intentionally NOT part of the mashalled data. + # In rspec the example group cannot deterministically being marshalled, because + # they reference a crazy mix of IO objects, global objects etc. + # + # @return [Array] + # + # @api private + # + def marshal_dump + [@output, @success] + end + + # Load marshalled data + # + # @param [Array] arry + # + # @return [undefined] + # + # @api private + # + def marshal_load(array) + @output, @success = array + end + + end # Report + + # Run tests + # + # @return [Test::Result] + # + # @api private + # + abstract_method :run + + # Return test identification + # + # @return [String] + # + # @api private + # + def identification + "#{self.class::PREFIX}:#{subject_identification}" + end + memoize :identification + + # Return subject identification + # + # This method is used for current mutants primitive test selection. + # + # @return [String] + # + # @api private + # + abstract_method :subject_identification + + end # Test +end # Mutant diff --git a/spec/integration/mutant/null_spec.rb b/spec/integration/mutant/null_spec.rb new file mode 100644 index 00000000..044a764b --- /dev/null +++ b/spec/integration/mutant/null_spec.rb @@ -0,0 +1,18 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe 'null integration' do + + let(:base_cmd) { 'bundle exec mutant -I lib --require test_app "::TestApp*"' } + + around do |example| + Dir.chdir(TestApp.root) do + example.run + end + end + + specify 'it allows to kill mutations' do + expect(Kernel.system(base_cmd)).to be(false) + end +end diff --git a/spec/integration/mutant/rspec_spec.rb b/spec/integration/mutant/rspec_spec.rb index 886ee3d3..a08141b6 100644 --- a/spec/integration/mutant/rspec_spec.rb +++ b/spec/integration/mutant/rspec_spec.rb @@ -10,7 +10,7 @@ describe 'rspec integration' do around do |example| Bundler.with_clean_env do Dir.chdir(TestApp.root) do - Kernel.system("bundle install --gemfile=#{gemfile}") + Kernel.system("bundle install --gemfile=#{gemfile}") || fail('Bundle install failed!') ENV['BUNDLE_GEMFILE'] = gemfile example.run end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3a3410c4..e62ede1a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,11 +2,9 @@ if ENV['COVERAGE'] == 'true' require 'simplecov' - require 'coveralls' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ SimpleCov::Formatter::HTMLFormatter, - Coveralls::SimpleCov::Formatter ] SimpleCov.start do diff --git a/spec/unit/mutant/diff_spec.rb b/spec/unit/mutant/diff_spec.rb new file mode 100644 index 00000000..eac09b3c --- /dev/null +++ b/spec/unit/mutant/diff_spec.rb @@ -0,0 +1,162 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe Mutant::Diff do + let(:object) { described_class } + + describe '.build' do + + subject { object.build(old_string, new_string) } + + let(:old_string) { "foo\nbar" } + let(:new_string) { "bar\nbaz" } + + it { should eql(Mutant::Diff.new(%w(foo bar), %w(bar baz))) } + + end + + describe '.colorize_line' do + let(:object) { described_class } + + subject { object.colorize_line(line) } + + context 'line beginning with "+"' do + let(:line) { '+line' } + + it { should eql(Mutant::Color::GREEN.format(line)) } + end + + context 'line beginning with "-"' do + let(:line) { '-line' } + + it { should eql(Mutant::Color::RED.format(line)) } + end + + context 'line beginning in other char' do + let(:line) { ' line' } + + it { should eql(line) } + end + end + + describe '#diff' do + let(:object) { described_class.new(old, new) } + + subject { object.diff } + + context 'when there is a diff at begin of hunk' do + let(:old) { %w(foo bar) } + let(:new) { %w(baz bar) } + + let(:expectation) do + strip_indent(<<-STR) + @@ -1,3 +1,3 @@ + -foo + +baz + bar + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when there is a diff NOT at begin of hunk' do + let(:old) { %w(foo bar) } + let(:new) { %w(foo baz bar) } + + let(:expectation) do + strip_indent(<<-STR) + @@ -1,3 +1,4 @@ + foo + +baz + bar + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when the diff has a long context at begin' do + let(:old) { %w(foo bar baz boz a b c) } + let(:new) { %w(foo bar baz boz a b c other) } + + let(:expectation) do + strip_indent(<<-STR) + @@ -1,8 +1,9 @@ + foo + bar + baz + boz + a + b + c + +other + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when the diff has a long context at end, deleting' do + let(:old) { %w(other foo bar baz boz a b c) } + let(:new) { %w(foo bar baz boz a b c) } + + let(:expectation) do + strip_indent(<<-STR) + @@ -1,9 +1,8 @@ + -other + foo + bar + baz + boz + a + b + c + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when the diff has a long context at end, inserting' do + let(:old) { %w(foo bar baz boz a b c) } + let(:new) { %w(other foo bar baz boz a b c) } + + let(:expectation) do + strip_indent(<<-STR) + @@ -1,8 +1,9 @@ + +other + foo + bar + baz + boz + a + b + c + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when there is no diff' do + let(:old) { '' } + let(:new) { '' } + + it { should be(nil) } + + it_should_behave_like 'an idempotent method' + end + end +end diff --git a/spec/unit/mutant/differ/diff_spec.rb b/spec/unit/mutant/differ/diff_spec.rb deleted file mode 100644 index ba86f6b0..00000000 --- a/spec/unit/mutant/differ/diff_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -describe Mutant::Differ, '#diff' do - let(:object) { described_class.new(old, new) } - - subject { object.diff } - - context 'when there is a diff at begin of hunk' do - let(:old) { %w(foo bar) } - let(:new) { %w(baz bar) } - - let(:expectation) do - strip_indent(<<-STR) - @@ -1,3 +1,3 @@ - -foo - +baz - bar - STR - end - - it { should eql(expectation) } - - it_should_behave_like 'an idempotent method' - end - - context 'when there is a diff NOT at begin of hunk' do - let(:old) { %w(foo bar) } - let(:new) { %w(foo baz bar) } - - let(:expectation) do - strip_indent(<<-STR) - @@ -1,3 +1,4 @@ - foo - +baz - bar - STR - end - - it { should eql(expectation) } - - it_should_behave_like 'an idempotent method' - end - - context 'when the diff has a long context at begin' do - let(:old) { %w(foo bar baz boz a b c) } - let(:new) { %w(foo bar baz boz a b c other) } - - let(:expectation) do - strip_indent(<<-STR) - @@ -1,8 +1,9 @@ - foo - bar - baz - boz - a - b - c - +other - STR - end - - it { should eql(expectation) } - - it_should_behave_like 'an idempotent method' - end - - context 'when the diff has a long context at end, deleting' do - let(:old) { %w(other foo bar baz boz a b c) } - let(:new) { %w(foo bar baz boz a b c) } - - let(:expectation) do - strip_indent(<<-STR) - @@ -1,9 +1,8 @@ - -other - foo - bar - baz - boz - a - b - c - STR - end - - it { should eql(expectation) } - - it_should_behave_like 'an idempotent method' - end - - context 'when the diff has a long context at end, inserting' do - let(:old) { %w(foo bar baz boz a b c) } - let(:new) { %w(other foo bar baz boz a b c) } - - let(:expectation) do - strip_indent(<<-STR) - @@ -1,8 +1,9 @@ - +other - foo - bar - baz - boz - a - b - c - STR - end - - it { should eql(expectation) } - - it_should_behave_like 'an idempotent method' - end - - context 'when there is no diff' do - let(:old) { '' } - let(:new) { '' } - - it { should be(nil) } - - it_should_behave_like 'an idempotent method' - end -end diff --git a/spec/unit/mutant/differ_spec.rb b/spec/unit/mutant/differ_spec.rb deleted file mode 100644 index 7804e5f9..00000000 --- a/spec/unit/mutant/differ_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -describe Mutant::Differ do - let(:object) { described_class } - - describe '.build' do - - subject { object.build(old_string, new_string) } - - let(:old_string) { "foo\nbar" } - let(:new_string) { "bar\nbaz" } - - it { should eql(Mutant::Differ.new(%w(foo bar), %w(bar baz))) } - - end - - describe '.colorize_line' do - let(:object) { described_class } - - subject { object.colorize_line(line) } - - context 'line beginning with "+"' do - let(:line) { '+line' } - - it { should eql(Mutant::Color::GREEN.format(line)) } - end - - context 'line beginning with "-"' do - let(:line) { '-line' } - - it { should eql(Mutant::Color::RED.format(line)) } - end - - context 'line beginning in other char' do - let(:line) { ' line' } - - it { should eql(line) } - end - end -end diff --git a/spec/unit/mutant/killer/success_predicate_spec.rb b/spec/unit/mutant/killer/success_predicate_spec.rb deleted file mode 100644 index d9c748ea..00000000 --- a/spec/unit/mutant/killer/success_predicate_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -describe Mutant::Killer, '#success?' do - subject { object.success? } - - let(:object) { class_under_test.new(strategy, mutation) } - let(:strategy) { double('Strategy') } - let(:mutation) { double('Mutation', success?: kill_state) } - let(:kill_state) { double('Kill State') } - - before do - kill_state.stub(freeze: kill_state, dup: kill_state) - end - - let(:class_under_test) do - Class.new(described_class) do - def run - end - end - end - - it_should_behave_like 'an idempotent method' - - it 'should use kill state to gather success' do - mutation.should_receive(:success?).with(object).and_return(kill_state) - should be(kill_state) - end -end diff --git a/spec/unit/mutant/mutation_spec.rb b/spec/unit/mutant/mutation_spec.rb index fac8bf2b..8ae7a09a 100644 --- a/spec/unit/mutant/mutation_spec.rb +++ b/spec/unit/mutant/mutation_spec.rb @@ -4,10 +4,13 @@ require 'spec_helper' describe Mutant::Mutation do - let(:class_under_test) { Class.new(described_class) { memoize :identification } } - let(:object) { class_under_test.new(mutation_subject, Mutant::NodeHelpers::N_NIL) } - let(:mutation_subject) { double('Subject', identification: 'subject', source: 'original') } - let(:node) { double('Node') } + class TestMutation < Mutant::Mutation + SYMBOL = 'test' + end + + let(:object) { TestMutation.new(mutation_subject, Mutant::NodeHelpers::N_NIL) } + let(:mutation_subject) { double('Subject', identification: 'subject', source: 'original') } + let(:node) { double('Node') } describe '#code' do subject { object.code } @@ -37,7 +40,7 @@ describe Mutant::Mutation do subject { object.identification } - it { should eql('subject:8771a') } + it { should eql('test:subject:8771a') } it_should_behave_like 'an idempotent method' end diff --git a/spec/unit/mutant/reporter/null_spec.rb b/spec/unit/mutant/reporter/null_spec.rb new file mode 100644 index 00000000..3f7841e2 --- /dev/null +++ b/spec/unit/mutant/reporter/null_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Mutant::Reporter::Null do + let(:object) { described_class.new } + + describe '#report' do + subject { object.report(double('some input')) } + + it_should_behave_like 'a command method' + end +end diff --git a/spec/unit/mutant/rspec/killer_spec.rb b/spec/unit/mutant/rspec/killer_spec.rb deleted file mode 100644 index fa07f496..00000000 --- a/spec/unit/mutant/rspec/killer_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' -require 'mutant-rspec' - -describe Mutant::Rspec::Killer, '.new' do - - before do - pending 'dactivated' - end - - subject { object.new(strategy, mutation) } - - let(:context) { double('Context') } - let(:mutation_subject) { double('Mutation Subject') } - - let(:object) { described_class } - - let(:mutation) do - double( - 'Mutation', - subject: mutation_subject, - should_survive?: false - ) - end - - let(:strategy) do - double( - 'Strategy', - spec_files: ['foo'], - error_stream: $stderr, - output_stream: $stdout - ) - end - - before do - mutation.stub(:insert) - mutation.stub(:reset) - RSpec::Core::Runner.stub(run: exit_status) - end - - context 'when run exits zero' do - let(:exit_status) { 0 } - - it { expect(subject.killed?).to be(false) } - - it { should be_a(described_class) } - end - - context 'when run exits nonzero' do - let(:exit_status) { 1 } - - it { expect(subject.killed?).to be(true) } - - it { should be_a(described_class) } - end -end diff --git a/spec/unit/mutant/runner/config_spec.rb b/spec/unit/mutant/runner/config_spec.rb index c8581dfd..114fcd7a 100644 --- a/spec/unit/mutant/runner/config_spec.rb +++ b/spec/unit/mutant/runner/config_spec.rb @@ -18,15 +18,14 @@ describe Mutant::Runner::Config do ) end - let(:fail_fast) { false } - let(:expected_coverage) { 100.0 } - let(:reporter) { double('Reporter') } - let(:strategy) { double('Strategy') } - let(:subject_a) { double('Subject A') } - let(:subject_b) { double('Subject B') } + let(:fail_fast) { false } + let(:expected_coverage) { 100.0 } + let(:reporter) { Mutant::Reporter::Trace.new } + let(:strategy) { double('Strategy') } + let(:subject_a) { double('Subject A') } + let(:subject_b) { double('Subject B') } before do - reporter.stub(report: reporter) strategy.stub(:setup) strategy.stub(:teardown) Mutant::Runner.stub(:run).with(config, subject_a).and_return(runner_a) diff --git a/spec/unit/mutant/runner/mutation/killer_spec.rb b/spec/unit/mutant/runner/mutation/killer_spec.rb deleted file mode 100644 index 9a402bd0..00000000 --- a/spec/unit/mutant/runner/mutation/killer_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -describe Mutant::Runner::Mutation, '#killer' do - let(:object) { described_class.run(config, mutation) } - - let(:config) do - double( - 'Config', - fail_fast: fail_fast, - reporter: reporter, - strategy: strategy - ) - end - - let(:reporter) { double('Reporter') } - let(:mutation) { double('Mutation', class: Mutant::Mutation) } - let(:strategy) { double('Strategy') } - let(:killer) { double('Killer', success?: success) } - let(:fail_fast) { false } - let(:success) { false } - - subject { object.killer } - - before do - reporter.stub(report: reporter) - strategy.stub(kill: killer) - end - - it 'should call configuration to identify strategy' do - config.should_receive(:strategy).with(no_args).and_return(strategy) - should be(killer) - end - - it 'should run killer' do - strategy.should_receive(:kill).with(mutation).and_return(killer) - should be(killer) - end - - it { should be(killer) } - - it_should_behave_like 'an idempotent method' -end diff --git a/spec/unit/mutant/runner/mutation_spec.rb b/spec/unit/mutant/runner/mutation_spec.rb new file mode 100644 index 00000000..c946205f --- /dev/null +++ b/spec/unit/mutant/runner/mutation_spec.rb @@ -0,0 +1,102 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe Mutant::Runner::Mutation do + let(:object) { described_class.new(config, mutation, tests) } + + let(:reporter) { double('Reporter') } + let(:mutation) { double('Mutation', class: Mutant::Mutation) } + let(:strategy) { double('Strategy') } + let(:killer_a) { Mutant::Killer.new(test: test_a, mutation: mutation) } + let(:killer_b) { Mutant::Killer.new(test: test_b, mutation: mutation) } + let(:runner_a) { double('Runner A', success?: success_a, stop?: stop_a) } + let(:runner_b) { double('Runner B', success?: success_b, stop?: stop_b) } + let(:runners) { [runner_a, runner_b] } + let(:killers) { [killer_a, killer_b] } + let(:fail_fast) { false } + let(:success_a) { true } + let(:success_b) { true } + let(:stop_a) { false } + let(:stop_b) { false } + let(:test_a) { double('test a') } + let(:test_b) { double('test b') } + let(:tests) { [test_a, test_b] } + + before do + expect(Mutant::Runner).to receive(:run).with(config, killer_a).and_return(runner_a) + expect(Mutant::Runner).to receive(:run).with(config, killer_b).and_return(runner_b) + end + + let(:config) do + double( + 'Config', + fail_fast: fail_fast, + reporter: reporter, + strategy: strategy + ) + end + + before do + reporter.stub(progress: reporter) + strategy.stub(killers: killers) + end + + describe '#stop?' do + subject { object.stop? } + + context 'when fail fast is false' do + it { should be(false) } + end + + context 'when fail fast is true' do + let(:fail_fast) { true } + + context 'when all killers are successful' do + it { should be(false) } + end + + context 'when one killer is NOT successful' do + let(:success_b) { false } + it { should be(false) } + end + + context 'when all killer are NOT successful' do + let(:success_b) { false } + let(:success_a) { false } + + it { should be(true) } + end + end + end + + describe '#success?' do + subject { object.success? } + + context 'when all killers are successful' do + it { should be(true) } + end + + context 'when one killer is not successful' do + let(:success_b) { false } + + it { should be(true) } + end + + context 'when all killer are not successful' do + let(:success_a) { false } + let(:success_b) { false } + + it { should be(false) } + end + end + + describe '#killers' do + subject { object.killers } + + + it { should eql(runners) } + + it_should_behave_like 'an idempotent method' + end +end diff --git a/spec/unit/mutant/runner/subject_spec.rb b/spec/unit/mutant/runner/subject_spec.rb index 5baf1ba6..96f81b57 100644 --- a/spec/unit/mutant/runner/subject_spec.rb +++ b/spec/unit/mutant/runner/subject_spec.rb @@ -15,10 +15,11 @@ describe Mutant::Runner::Subject, '#success?' do ) end - let(:reporter) { double('Reporter') } - let(:config) { double('Config', reporter: reporter) } - let(:mutation_a) { double('Mutation A') } - let(:mutation_b) { double('Mutation B') } + let(:reporter) { Mutant::Reporter::Trace.new } + let(:config) { double('Config', reporter: reporter, strategy: strategy) } + let(:mutation_a) { double('Mutation A') } + let(:mutation_b) { double('Mutation B') } + let(:strategy) { double('Strategy') } let(:runner_a) do double('Runner A', success?: success_a, stop?: stop_a) @@ -28,10 +29,12 @@ describe Mutant::Runner::Subject, '#success?' do double('Runner B', success?: success_b, stop?: stop_b) end + let(:tests) { [double('test a'), double('test b')] } + before do - reporter.stub(report: reporter) - Mutant::Runner.stub(:run).with(config, mutation_a).and_return(runner_a) - Mutant::Runner.stub(:run).with(config, mutation_b).and_return(runner_b) + expect(strategy).to receive(:tests).with(mutation_subject).and_return(tests) + expect(Mutant::Runner).to receive(:run).with(config, mutation_a, tests).and_return(runner_a) + expect(Mutant::Runner).to receive(:run).with(config, mutation_b, tests).and_return(runner_b) end context 'with failing mutations' do diff --git a/spec/unit/mutant_spec.rb b/spec/unit/mutant_spec.rb index d5779655..8443e50d 100644 --- a/spec/unit/mutant_spec.rb +++ b/spec/unit/mutant_spec.rb @@ -38,4 +38,57 @@ describe Mutant do expect(inspect).to be_frozen end end + + describe '.isolate' do + let(:object) { described_class } + + let(:expected_return) { :foo } + + subject { object.isolate(&block) } + + def redirect_stderr + $stderr = File.open('/dev/null') + end + + unless ENV['COVERAGE'] + context 'when block returns mashallable data, and process exists zero' do + let(:block) do + lambda do + :data_from_child_process + end + end + + it { should eql(:data_from_child_process) } + end + end + + context 'when block does return marshallable data' do + let(:block) do + lambda do + redirect_stderr + $stderr # not mashallable, nothing written to pipe and raised exceptions in child + end + end + + it 'raises an exception' do + expect { subject }.to raise_error(Mutant::IsolationError, 'Childprocess wrote un-unmarshallable data') + end + end + + context 'when block does return marshallable data, but process exits with nonzero exitstatus' do + let(:block) do + lambda do + redirect_stderr + at_exit do + raise + end + :foo + end + end + + it 'raises an exception' do + expect { subject }.to raise_error(Mutant::IsolationError, 'Childprocess exited with nonzero exit status: 1') + end + end + end end