diff --git a/Gemfile b/Gemfile index 184b9a3b..23cb128c 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' gemspec -gem 'mutant', :path => '.' +gem 'mutant', path: '.' -gem 'devtools', :git => 'https://github.com/rom-rb/devtools.git' -eval(File.read(File.join(File.dirname(__FILE__),'Gemfile.devtools'))) +gem 'devtools', git: 'https://github.com/rom-rb/devtools.git' +eval(File.read(File.join(File.dirname(__FILE__), 'Gemfile.devtools'))) diff --git a/Gemfile.devtools b/Gemfile.devtools index 3270139c..00c1f2e1 100644 --- a/Gemfile.devtools +++ b/Gemfile.devtools @@ -14,26 +14,27 @@ group :guard do gem 'guard', '~> 1.8.1' gem 'guard-bundler', '~> 1.0.0' gem 'guard-rspec', '~> 3.0.2' -# gem 'guard-rubocop', '~> 0.2.0' + gem 'guard-rubocop', '~> 0.2.0' + gem 'guard-mutant', '~> 0.0.1' # file system change event handling gem 'listen', '~> 1.2.2' - gem 'rb-fchange', '~> 0.0.6', :require => false - gem 'rb-fsevent', '~> 0.9.3', :require => false - gem 'rb-inotify', '~> 0.9.0', :require => false + gem 'rb-fchange', '~> 0.0.6', require: false + gem 'rb-fsevent', '~> 0.9.3', require: false + gem 'rb-inotify', '~> 0.9.0', require: false # notification handling - gem 'libnotify', '~> 0.8.0', :require => false - gem 'rb-notifu', '~> 0.0.4', :require => false - gem 'terminal-notifier-guard', '~> 1.5.3', :require => false + gem 'libnotify', '~> 0.8.0', require: false + gem 'rb-notifu', '~> 0.0.4', require: false + gem 'terminal-notifier-guard', '~> 1.5.3', require: false end group :metrics do gem 'coveralls', '~> 0.6.7' gem 'flay', '~> 2.3.1' gem 'flog', '~> 4.1.1' - gem 'reek', '~> 1.3.1', :git => 'https://github.com/troessner/reek.git' -# gem 'rubocop', '~> 0.9.1' + gem 'reek', '~> 1.3.1', git: 'https://github.com/troessner/reek.git' + gem 'rubocop', '~> 0.10.0', git: 'https://github.com/bbatsov/rubocop.git' gem 'simplecov', '~> 0.7.1' gem 'yardstick', '~> 0.9.6' @@ -41,10 +42,6 @@ group :metrics do gem 'yard-spellcheck', '~> 0.1.5' end - platforms :ruby_19 do - gem 'json', '~> 1.8.0' - end - platforms :rbx do gem 'pelusa', '~> 0.2.2' end diff --git a/README.md b/README.md index 41ad6b6b..f3ded1f2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ mutant [![Dependency Status](https://gemnasium.com/mbj/mutant.png)](https://gemnasium.com/mbj/mutant) [![Code Climate](https://codeclimate.com/github/mbj/mutant.png)](https://codeclimate.com/github/mbj/mutant) -Mutant is a mutation testing tool for ruby. It aims to be better than existing mutation testers. +Mutant is a mutation testing tool for ruby. The idea is that if code can be changed and your tests do not notice, either that code isn't being covered or it does not have a speced side effect. @@ -47,6 +47,36 @@ The 0.2 series is stable but has outdated dependencies. The 0.3 series is in bet gem install mutant --pre ``` +Mutations +--------- + +Mutant supports a very wide range of mutation operators. Listing them all in detail would blow this document up. + +It is planned to parse a list of mutation operators from the source. In the meantime please refer to the +[code](https://github.com/mbj/mutant/tree/master/lib/mutant/mutator/node) each subclass of `Mutant::Mutator::Node` +emits around 3-6 mutations. + +Currently mutant covers the majority of ruby's complex nodes that often occur in method bodies. + +A some stats from the [axiom](https://github.com/dkubb/axiom) library: + +``` +Subjects: 417 # Amount of subjects being mutated (currently only methods) +Mutations: 5442 # Amount of mutations mutant generated (~13 mutations per method) +Kills: 5385 # Amount of successfully killed mutations +Runtime: 1898.11s # Total runtime +Killtime: 1884.17s # Time spend killing mutations +Overhead: 0.73% +Coverage: 98.95% # Coverage score +Alive: 57 # Amount of alive mutations. +``` + + +Nodes still missing a dedicated mutator are handled via the +[Generic](https://github.com/mbj/mutant/blob/master/lib/mutant/mutator/node/generic.rb) mutator. +The goal is to remove this mutator and have dedicated mutator for every type of node and removing +the Generic handler altogether. + Examples -------- diff --git a/bin/mutant b/bin/mutant index 9daca978..9248ea4a 100755 --- a/bin/mutant +++ b/bin/mutant @@ -1,14 +1,14 @@ #!/usr/bin/env ruby trap('INT') do |status| - exit! 128+status + exit! 128 + status end require 'mutant' namespace = - if File.basename($0) == 'zombie' - $stderr.puts('Detected zombie environment...') + if ARGV.include?('--zombie') + $stderr.puts('Running mutant zombified!') Mutant::Zombifier.zombify Zombie::Mutant else diff --git a/bin/zombie b/bin/zombie deleted file mode 120000 index 9dde3a27..00000000 --- a/bin/zombie +++ /dev/null @@ -1 +0,0 @@ -mutant \ No newline at end of file diff --git a/config/flay.yml b/config/flay.yml index 1fc1ba65..f752d954 100644 --- a/config/flay.yml +++ b/config/flay.yml @@ -1,3 +1,3 @@ --- threshold: 16 -total_score: 685 +total_score: 737 diff --git a/config/reek.yml b/config/reek.yml index b0e65c3f..41fd342b 100644 --- a/config/reek.yml +++ b/config/reek.yml @@ -47,6 +47,7 @@ NestedIterators: exclude: - Mutant#self.singleton_subclass_instance - Mutant::Mutator::Util::Array::Element#dispatch + - Mutant::Reporter::CLI::Printer::Config::Runner#generic_stats - Mutant::CLI#parse max_allowed_nesting: 1 ignore_iterators: [] diff --git a/config/rubocop.yml b/config/rubocop.yml index af82f0cb..9f3d5100 100644 --- a/config/rubocop.yml +++ b/config/rubocop.yml @@ -1,8 +1,11 @@ AllCops: Includes: - '../**/*.rake' + - 'Gemfile' + - 'Gemfile.devtools' Excludes: - - '../vendor/**' + - '**/vendor/**' + - '**/benchmarks/**' # Avoid parameter lists longer than five parameters. ParameterLists: diff --git a/lib/mutant.rb b/lib/mutant.rb index bc540863..7f2dd863 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -20,7 +20,9 @@ require 'concord' # Library namespace module Mutant -end + # The empty string used within this namespace + EMPTY_STRING = ''.freeze +end # Mutant require 'mutant/cache' require 'mutant/node_helpers' @@ -52,16 +54,21 @@ require 'mutant/mutator/node/literal/array' require 'mutant/mutator/node/literal/hash' require 'mutant/mutator/node/literal/regex' require 'mutant/mutator/node/literal/nil' -require 'mutant/mutator/node/assignment' require 'mutant/mutator/node/argument' require 'mutant/mutator/node/arguments' require 'mutant/mutator/node/begin' +require 'mutant/mutator/node/cbase' +require 'mutant/mutator/node/connective/binary' +require 'mutant/mutator/node/const' +require 'mutant/mutator/node/named_value/access' +require 'mutant/mutator/node/named_value/constant_assignment' +require 'mutant/mutator/node/named_value/variable_assignment' require 'mutant/mutator/node/while' require 'mutant/mutator/node/super' +require 'mutant/mutator/node/zsuper' require 'mutant/mutator/node/send' require 'mutant/mutator/node/send/binary' require 'mutant/mutator/node/when' -require 'mutant/mutator/node/assignment' require 'mutant/mutator/node/define' require 'mutant/mutator/node/mlhs' require 'mutant/mutator/node/masgn' diff --git a/lib/mutant/cli.rb b/lib/mutant/cli.rb index efda41dc..466eb35e 100644 --- a/lib/mutant/cli.rb +++ b/lib/mutant/cli.rb @@ -202,6 +202,8 @@ module Mutant opts.separator '' opts.separator 'Strategies:' + opts.on('--zombie', 'Run mutant zombified') + add_strategies(opts) add_options(opts) end diff --git a/lib/mutant/cli/classifier.rb b/lib/mutant/cli/classifier.rb index c74f8259..f25ebe1e 100644 --- a/lib/mutant/cli/classifier.rb +++ b/lib/mutant/cli/classifier.rb @@ -10,7 +10,8 @@ module Mutant OPERATOR_PATTERN = Regexp.union(*OPERATOR_METHODS.map(&:to_s)).freeze METHOD_NAME_PATTERN = /([_A-Za-z][A-Za-z0-9_]*[!?=]?|#{OPERATOR_PATTERN})/.freeze SCOPE_PATTERN = /(?:::)?#{SCOPE_NAME_PATTERN}(?:::#{SCOPE_NAME_PATTERN})*/.freeze - + CBASE_PATTERN = /\A::/.freeze + SCOPE_OPERATOR = '::'.freeze SINGLETON_PATTERN = %r(\A(#{SCOPE_PATTERN})\z).freeze REGISTRY = [] @@ -35,7 +36,7 @@ module Mutant # @api private # def self.constant_lookup(location) - location.gsub(%r(\A::), '').split('::').inject(Object) do |parent, name| + location.gsub(CBASE_PATTERN, EMPTY_STRING).split(SCOPE_OPERATOR).inject(Object) do |parent, name| parent.const_get(name) end end diff --git a/lib/mutant/constants.rb b/lib/mutant/constants.rb index 403336b9..92106e84 100644 --- a/lib/mutant/constants.rb +++ b/lib/mutant/constants.rb @@ -71,7 +71,7 @@ module Mutant :kwarg, :restarg, :arg, :block_pass, :or, :and, :next, :undef, :if, :module, :cbase, :block, :send, :zsuper, :super, :empty, :alias, :for, :redo, - :return, :splat, :not, :defined?, :op_asgn, :self, + :return, :splat, :defined?, :op_asgn, :self, :true, :false, :nil, :dstr, :dsym, :regexp, :regopt, :int, :str, :float, :sym, :pair, :hash, :array, :xstr, :def, :defs, :case, :when, :ivar, :lvar, :cvar, :gvar, diff --git a/lib/mutant/killer/rspec.rb b/lib/mutant/killer/rspec.rb index 4935dea5..c05fb454 100644 --- a/lib/mutant/killer/rspec.rb +++ b/lib/mutant/killer/rspec.rb @@ -18,14 +18,21 @@ module Mutant def run mutation.insert # TODO: replace with real streams from configuration + # Note: we assume the only interesting output from a failed rspec run is stderr. require 'stringio' - null = StringIO.new - argv = command_line_arguments - begin - !::RSpec::Core::Runner.run(argv, null, null).zero? - rescue StandardError - true + rspec_err = StringIO.new + + killed = !::RSpec::Core::Runner.run(command_line_arguments, nil, rspec_err).zero? + + if killed and mutation.should_survive? + rspec_err.rewind + + puts "#{mutation.class} test failed." + puts 'RSpec stderr:' + puts rspec_err.read end + + killed end # Return command line arguments diff --git a/lib/mutant/mutation.rb b/lib/mutant/mutation.rb index 1cf46749..05595d0c 100644 --- a/lib/mutant/mutation.rb +++ b/lib/mutant/mutation.rb @@ -28,6 +28,18 @@ module Mutant # abstract_method :success? + # Indicate if a killer should treat a kill as problematic + # + # @return [true] + # if killing is unexpected + # + # @return [false] + # if killing is expected + # + # @api private + # + abstract_method :should_survive? + # Insert mutated node # # FIXME: diff --git a/lib/mutant/mutation/evil.rb b/lib/mutant/mutation/evil.rb index f01a781b..2ca1ce2c 100644 --- a/lib/mutant/mutation/evil.rb +++ b/lib/mutant/mutation/evil.rb @@ -30,6 +30,16 @@ module Mutant killer.killed? end + # Indicate if a killer should treat a kill as problematic. + # + # @return [false] Killing evil mutants is not problematic. + # + # @api private + # + def should_survive? + false + end + end # Evil end # Mutation end # Mutant diff --git a/lib/mutant/mutation/neutral.rb b/lib/mutant/mutation/neutral.rb index 956daad0..c4f77e67 100644 --- a/lib/mutant/mutation/neutral.rb +++ b/lib/mutant/mutation/neutral.rb @@ -10,6 +10,19 @@ module Mutant SYMBOL = 'noop' + # Indicate if a killer should treat a kill as problematic. + # + # @return [false] Killing noop mutants is a serious problem. Failures + # in noop may indicate a broken test suite, but they can also be an + # indication mutant has altered the runtime environment in a subtle + # way and tickled an odd bug. + # + # @api private + # + def should_survive? + false + end + end # Return identification @@ -39,6 +52,16 @@ module Mutant !killer.killed? end + # Indicate if a killer should treat a kill as problematic. + # + # @return [true] Neutral mutants must die. + # + # @api private + # + def should_survive? + false + end + end # Neutral end # Mutation end # Mutant diff --git a/lib/mutant/mutator/node/begin.rb b/lib/mutant/mutator/node/begin.rb index cda4cefc..23bf4e5e 100644 --- a/lib/mutant/mutator/node/begin.rb +++ b/lib/mutant/mutator/node/begin.rb @@ -21,7 +21,8 @@ module Mutant emit_self(*children) end end - children.each do |child| + children.each_with_index do |child, index| + mutate_child(index) emit(child) end end diff --git a/lib/mutant/mutator/node/cbase.rb b/lib/mutant/mutator/node/cbase.rb new file mode 100644 index 00000000..fb6a1989 --- /dev/null +++ b/lib/mutant/mutator/node/cbase.rb @@ -0,0 +1,25 @@ +module Mutant + class Mutator + class Node + + # Mutation emitter to handle cbase nodes + class Cbase < self + + handle(:cbase) + + private + + # Emit mutations + # + # @return [undefined] + # + # @api private + # + def dispatch + # noop, for now + end + + end # Cbase + end # Node + end # Mutator +end # Mutant diff --git a/lib/mutant/mutator/node/connective/binary.rb b/lib/mutant/mutator/node/connective/binary.rb new file mode 100644 index 00000000..44b17360 --- /dev/null +++ b/lib/mutant/mutator/node/connective/binary.rb @@ -0,0 +1,59 @@ +module Mutant + class Mutator + class Node + module Connective + + # Mutation emitter to handle binary connectives + class Binary < Node + + INVERSE = { + :and => :or, + :or => :and, + }.freeze + + handle *INVERSE.keys + + children :left, :right + + private + + # Emit mutations + # + # @return [undefined] + # + # @api private + # + def dispatch + emit_nil + emit(left) + emit(right) + mutate_operator + mutate_operands + end + + # Emit operator mutations + # + # @return [undefined] + # + # @api private + # + def mutate_operator + emit(s(INVERSE.fetch(node.type), left, right)) + end + + # Emit condition mutations + # + # @return [undefined] + # + # @api private + # + def mutate_operands + emit(s(node.type, n_not(left), right)) + emit(n_not(node)) + end + + end # Binary + end # Connective + end # Node + end # Mutator +end # Mutant diff --git a/lib/mutant/mutator/node/const.rb b/lib/mutant/mutator/node/const.rb new file mode 100644 index 00000000..898e05ce --- /dev/null +++ b/lib/mutant/mutator/node/const.rb @@ -0,0 +1,28 @@ +module Mutant + class Mutator + class Node + + # Mutation emitter to handle const nodes + class Const < self + + handle(:const) + + private + + # Emit mutations + # + # @return [undefined] + # + # @api private + # + def dispatch + emit_nil + children.each_with_index do |child, index| + mutate_child(index) if child.kind_of?(Parser::AST::Node) + end + end + + end # Const + end # Node + end # Mutator +end # Mutant diff --git a/lib/mutant/mutator/node/generic.rb b/lib/mutant/mutator/node/generic.rb index 8a4985fe..149b9782 100644 --- a/lib/mutant/mutator/node/generic.rb +++ b/lib/mutant/mutator/node/generic.rb @@ -1,20 +1,19 @@ module Mutant class Mutator class Node + # Generic mutator class Generic < self - handle(:self) - # These nodes still need a dedicated mutator, # your contribution is that close! handle( - :zsuper, :not, :or, :and, :defined, - :next, :break, :match, :gvar, :cvar, :ensure, + :defined, + :next, :break, :match, :ensure, :dstr, :dsym, :yield, :rescue, :redo, :defined?, - :lvar, :const, :blockarg, :block_pass, :op_asgn, :and_asgn, - :regopt, :ivar, :restarg, :casgn, :resbody, :retry, :arg_expr, - :kwrestarg, :kwoptarg, :kwarg, :undef, :module, :cbase, :empty, + :blockarg, :block_pass, :op_asgn, :and_asgn, + :regopt, :restarg, :resbody, :retry, :arg_expr, + :kwrestarg, :kwoptarg, :kwarg, :undef, :module, :empty, :alias, :for, :xstr, :back_ref, :nth_ref, :class, :sclass, :match_with_lvasgn, :match_current_line, :or_asgn, :kwbegin ) @@ -29,8 +28,7 @@ module Mutant # def dispatch children.each_with_index do |child, index| - next unless child.kind_of?(Parser::AST::Node) - mutate_child(index) + mutate_child(index) if child.kind_of?(Parser::AST::Node) end end diff --git a/lib/mutant/mutator/node/if.rb b/lib/mutant/mutator/node/if.rb index 9f9bd22e..178a7c11 100644 --- a/lib/mutant/mutator/node/if.rb +++ b/lib/mutant/mutator/node/if.rb @@ -30,9 +30,9 @@ module Mutant # def mutate_condition emit_condition_mutations - emit_self(s(:send, condition, :!), if_branch, else_branch) - emit_self(s(:true), if_branch, else_branch) - emit_self(s(:false), if_branch, else_branch) + emit_self(n_not(condition), if_branch, else_branch) + emit_self(N_TRUE, if_branch, else_branch) + emit_self(N_FALSE, if_branch, else_branch) end # Emit if branch mutations @@ -45,7 +45,7 @@ module Mutant emit_self(condition, else_branch, nil) if else_branch if if_branch emit_if_branch_mutations - emit_self(condition, if_branch, nil) + emit_self(condition, if_branch, nil) end end diff --git a/lib/mutant/mutator/node/literal/regex.rb b/lib/mutant/mutator/node/literal/regex.rb index 91ce6f69..23310870 100644 --- a/lib/mutant/mutator/node/literal/regex.rb +++ b/lib/mutant/mutator/node/literal/regex.rb @@ -7,15 +7,21 @@ module Mutant handle(:regexp) - EMPTY_STRING = ''.freeze - # No input can ever be matched with this NULL_REGEXP_SOURCE = 'a\A'.freeze - children :source, :options - private + # Return options + # + # @return [Parser::AST::Node] + # + # @api private + # + def options + children.last + end + # Emit mutants # # @return [undefined] @@ -24,6 +30,9 @@ module Mutant # def dispatch emit_nil + children.each_with_index do |child, index| + mutate_child(index) unless child.type == :str + end emit_self(s(:str, EMPTY_STRING), options) emit_self(s(:str, NULL_REGEXP_SOURCE), options) end diff --git a/lib/mutant/mutator/node/masgn.rb b/lib/mutant/mutator/node/masgn.rb index a95f4207..4e80e4e8 100644 --- a/lib/mutant/mutator/node/masgn.rb +++ b/lib/mutant/mutator/node/masgn.rb @@ -2,7 +2,7 @@ module Mutant class Mutator class Node - # Mutation emitter to handle multipl assignment nodes + # Mutation emitter to handle multiple assignment nodes class MultipleAssignment < self handle(:masgn) diff --git a/lib/mutant/mutator/node/named_value/access.rb b/lib/mutant/mutator/node/named_value/access.rb new file mode 100644 index 00000000..f34a8502 --- /dev/null +++ b/lib/mutant/mutator/node/named_value/access.rb @@ -0,0 +1,27 @@ +module Mutant + class Mutator + class Node + module NamedValue + + # Mutation emitter to handle named value access nodes + class Access < Node + + handle(:gvar, :cvar, :ivar, :lvar, :self) + + private + + # Emit mutations + # + # @return [undefined] + # + # @api private + # + def dispatch + emit_nil + end + + end # Access + end # NamedValue + end # Node + end # Mutator +end # Mutant diff --git a/lib/mutant/mutator/node/named_value/constant_assignment.rb b/lib/mutant/mutator/node/named_value/constant_assignment.rb new file mode 100644 index 00000000..00a63ea7 --- /dev/null +++ b/lib/mutant/mutator/node/named_value/constant_assignment.rb @@ -0,0 +1,42 @@ +module Mutant + class Mutator + class Node + module NamedValue + + # Mutation emitter to handle constant assignment nodes + class ConstantAssignment < Node + + children :cbase, :name, :value + + handle :casgn + + private + + # Perform dispatch + # + # @return [undefined] + # + # @api private + # + def dispatch + mutate_name + emit_value_mutations if value + end + + # Emit name mutations + # + # @return [undefined] + # + # @api private + # + def mutate_name + Mutator::Util::Symbol.each(name, self) do |name| + emit_name(name.upcase) + end + end + + end # ConstantAssignment + end # NamedValue + end # Node + end # Mutator +end # Mutant diff --git a/lib/mutant/mutator/node/assignment.rb b/lib/mutant/mutator/node/named_value/variable_assignment.rb similarity index 78% rename from lib/mutant/mutator/node/assignment.rb rename to lib/mutant/mutator/node/named_value/variable_assignment.rb index 520a8f73..60f3dd46 100644 --- a/lib/mutant/mutator/node/assignment.rb +++ b/lib/mutant/mutator/node/named_value/variable_assignment.rb @@ -1,11 +1,10 @@ module Mutant class Mutator class Node - # Mutator base class for assignments - class Assignment < self + module NamedValue - # Mutator for variable assignment - class Variable < self + # Mutation emitter to handle variable assignment nodes + class VariableAssignment < Node children :name, :value @@ -40,12 +39,12 @@ module Mutant def mutate_name prefix = MAP.fetch(node.type) Mutator::Util::Symbol.each(name, self) do |name| - emit_name("#{prefix}#{name}") + emit_name(prefix + name.to_s) end end - end # Variable - end # Assignment + end # VariableAssignment + end # NamedValue end # Node end # Mutator end # Mutant diff --git a/lib/mutant/mutator/node/super.rb b/lib/mutant/mutator/node/super.rb index 2770c1a1..940f4883 100644 --- a/lib/mutant/mutator/node/super.rb +++ b/lib/mutant/mutator/node/super.rb @@ -2,7 +2,7 @@ module Mutant class Mutator class Node - # Mutator for super with parantheses + # Mutator for super with parentheses class Super < self handle(:super) diff --git a/lib/mutant/mutator/node/zsuper.rb b/lib/mutant/mutator/node/zsuper.rb new file mode 100644 index 00000000..7e86e860 --- /dev/null +++ b/lib/mutant/mutator/node/zsuper.rb @@ -0,0 +1,25 @@ +module Mutant + class Mutator + class Node + + # Mutator for super without parentheses + class ZSuper < self + + handle(:zsuper) + + private + + # Emit mutations + # + # @return [undefined] + # + # @api private + # + def dispatch + emit_nil + end + + end # ZSuper + end # Node + end # Mutator +end # Mutant diff --git a/lib/mutant/node_helpers.rb b/lib/mutant/node_helpers.rb index 9b07316b..183f0afb 100644 --- a/lib/mutant/node_helpers.rb +++ b/lib/mutant/node_helpers.rb @@ -15,7 +15,6 @@ module Mutant end module_function :s - NAN = s(:send, s(:float, 0.0), :/, s(:args, s(:float, 0.0))) NEGATIVE_INFINITY = s(:send, s(:float, -1.0), :/, s(:args, s(:float, 0.0))) INFINITY = s(:send, s(:float, 1.0), :/, s(:args, s(:float, 0.0))) @@ -23,8 +22,22 @@ module Mutant RAISE = s(:send, nil, :raise) + N_TRUE = s(:true) + N_FALSE = s(:false) N_NIL = s(:nil) N_EMPTY = s(:empty) + # Build a negated boolean node + # + # @param [Parser::AST::Node] node + # + # @return [Parser::AST::Node] + # + # @api private + # + def n_not(node) + s(:send, node, :!) + end + end # NodeHelpers end # Mutant diff --git a/lib/mutant/reporter/cli/printer/config.rb b/lib/mutant/reporter/cli/printer/config.rb index fc20d38d..0551580e 100644 --- a/lib/mutant/reporter/cli/printer/config.rb +++ b/lib/mutant/reporter/cli/printer/config.rb @@ -45,6 +45,7 @@ module Mutant info 'Overhead: %0.2f%%', overhead status 'Coverage: %0.2f%%', coverage status 'Alive: %s', amount_alive + print_generic_stats self end @@ -60,6 +61,81 @@ module Mutant object.subjects end + # Walker for all ast nodes + class Walker + + # Run walkter + # + # @param [Parser::AST::Node] root + # + # @return [self] + # + # @api private + # + def self.run(root, &block) + new(root, block) + self + end + + private_class_method :new + + # Initialize and run walker + # + # @param [Parser::AST::Node] root + # @param [#call(node)] block + # + # @return [undefined] + # + # @api private + # + def initialize(root, block) + @root, @block = root, block + dispatch(root) + end + + private + + # Perform dispatch + # + # @return [undefined] + # + # @api private + # + def dispatch(node) + @block.call(node) + node.children.grep(Parser::AST::Node).each(&method(:dispatch)) + end + end + + # Print generic stats + # + # @return [undefined] + # + # @api private + # + def print_generic_stats + stats = generic_stats.to_a.sort_by(&:last) + 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 + object.subjects.each_with_object(Hash.new(0)) do |runner, stats| + Walker.run(runner.subject.node) do |node| + next unless Mutator::Registry.lookup(node) == Mutator::Node::Generic + stats[node.type] += 1 + end + end + end + # Return amount of subjects # # @return [Fixnum] diff --git a/mutant.gemspec b/mutant.gemspec index 2ab17114..b7206c62 100644 --- a/mutant.gemspec +++ b/mutant.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |gem| gem.name = 'mutant' - gem.version = '0.3.0.beta17' + gem.version = '0.3.0.beta21' gem.authors = [ 'Markus Schirp' ] gem.email = [ 'mbj@schirp-dso.com' ] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a25c8451..1e8c9795 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -24,7 +24,7 @@ RSpec.configure do |config| config.include(CompressHelper) config.include(ParserHelper) config.include(Mutant::NodeHelpers) - config.mock_with :rspec do |config| - config.syntax = [:expect, :should] + config.mock_with :rspec do |rspec| + rspec.syntax = [:expect, :should] end end diff --git a/spec/unit/mutant/killer/rspec/class_methods/new_spec.rb b/spec/unit/mutant/killer/rspec/class_methods/new_spec.rb index da34bc94..252cdb65 100644 --- a/spec/unit/mutant/killer/rspec/class_methods/new_spec.rb +++ b/spec/unit/mutant/killer/rspec/class_methods/new_spec.rb @@ -6,7 +6,7 @@ describe Mutant::Killer::Rspec, '.new' do let(:strategy) { double('Strategy', :spec_files => ['foo'], :error_stream => $stderr, :output_stream => $stdout) } let(:context) { double('Context') } - let(:mutation) { double('Mutation', :subject => mutation_subject) } + let(:mutation) { double('Mutation', :subject => mutation_subject, :should_survive? => false) } let(:mutation_subject) { double('Mutation Subject') } let(:object) { described_class } diff --git a/spec/unit/mutant/mutator/node/cbase/mutation_spec.rb b/spec/unit/mutant/mutator/node/cbase/mutation_spec.rb new file mode 100644 index 00000000..1ef7bef0 --- /dev/null +++ b/spec/unit/mutant/mutator/node/cbase/mutation_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Mutant::Mutator::Node::NamedValue::Access, 'cbase' do + + before do + Mutant::Random.stub(:hex_string => :random) + end + + let(:source) { '::A' } + + let(:mutations) do + mutants = [] + mutants << 'nil' + end + + it_should_behave_like 'a mutator' +end diff --git a/spec/unit/mutant/mutator/node/connective/binary/mutation_spec.rb b/spec/unit/mutant/mutator/node/connective/binary/mutation_spec.rb new file mode 100644 index 00000000..a51012d1 --- /dev/null +++ b/spec/unit/mutant/mutator/node/connective/binary/mutation_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Mutant::Mutator::Node::Connective::Binary, 'mutations' do + context 'and' do + let(:source) { 'true and false' } + + let(:mutations) do + mutations = [] + mutations << 'nil' + mutations << 'true' + mutations << 'false' + + mutations << 'true or false' + + mutations << 'not true and false' + mutations << 'not(true and false)' + end + + it_should_behave_like 'a mutator' + end + + context 'or' do + let(:source) { 'true or false' } + + let(:mutations) do + mutations = [] + mutations << 'nil' + mutations << 'true' + mutations << 'false' + + mutations << 'true and false' + + mutations << 'not true or false' + mutations << 'not(true or false)' + end + + it_should_behave_like 'a mutator' + end +end diff --git a/spec/unit/mutant/mutator/node/const/mutation_spec.rb b/spec/unit/mutant/mutator/node/const/mutation_spec.rb new file mode 100644 index 00000000..605939e4 --- /dev/null +++ b/spec/unit/mutant/mutator/node/const/mutation_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Mutant::Mutator::Node::NamedValue::Access, 'const' do + + before do + Mutant::Random.stub(:hex_string => :random) + end + + let(:source) { 'A::B' } + + let(:mutations) do + mutants = [] + mutants << 'nil' + mutants << 'nil::B' + end + + it_should_behave_like 'a mutator' +end diff --git a/spec/unit/mutant/mutator/node/literal/regex_spec.rb b/spec/unit/mutant/mutator/node/literal/regex_spec.rb index 36b08123..07cdba40 100644 --- a/spec/unit/mutant/mutator/node/literal/regex_spec.rb +++ b/spec/unit/mutant/mutator/node/literal/regex_spec.rb @@ -2,14 +2,31 @@ require 'spec_helper' describe Mutant::Mutator::Node::Literal, 'regex' do - let(:source) { '/foo/' } + context 'literal' do + let(:source) { '/foo/' } - let(:mutations) do - mutations = [] - mutations << 'nil' - mutations << '//' # match all - mutations << '/a\A/' # match nothing + let(:mutations) do + mutations = [] + mutations << 'nil' + mutations << '//' # match all + mutations << '/a\A/' # match nothing + end + + it_should_behave_like 'a mutator' + end + + context 'interpolated' do + let(:source) { '/#{foo.bar}n/' } + + let(:mutations) do + mutations = [] + mutations << 'nil' + mutations << '//' # match all + mutations << '/#{foo}n/' # match all + mutations << '/a\A/' # match nothing + end + + it_should_behave_like 'a mutator' end - it_should_behave_like 'a mutator' end diff --git a/spec/unit/mutant/mutator/node/masgn/mutation_spec.rb b/spec/unit/mutant/mutator/node/masgn/mutation_spec.rb index e5669c64..f4dd06d5 100644 --- a/spec/unit/mutant/mutator/node/masgn/mutation_spec.rb +++ b/spec/unit/mutant/mutator/node/masgn/mutation_spec.rb @@ -6,7 +6,6 @@ describe Mutant::Mutator, 'masgn' do Mutant::Random.stub(:hex_string => 'random') end - let(:source) { 'a, b = c, d' } let(:mutations) do diff --git a/spec/unit/mutant/mutator/node/named_value/access/mutation_spec.rb b/spec/unit/mutant/mutator/node/named_value/access/mutation_spec.rb new file mode 100644 index 00000000..680f6b80 --- /dev/null +++ b/spec/unit/mutant/mutator/node/named_value/access/mutation_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Mutant::Mutator::Node::NamedValue::Access, 'mutations' do + + before do + Mutant::Random.stub(:hex_string => :random) + end + + context 'global variable' do + let(:source) { '$a = nil; $a' } + + let(:mutations) do + mutants = [] + mutants << '$a = nil; nil' + mutants << '$a = nil' + mutants << '$a' + mutants << '$a = ::Object.new; $a' + mutants << '$srandom = nil; $a' + end + + it_should_behave_like 'a mutator' + end + + context 'class variable' do + let(:source) { '@@a = nil; @@a' } + + let(:mutations) do + mutants = [] + mutants << '@@a = nil; nil' + mutants << '@@a = nil' + mutants << '@@a' + mutants << '@@a = ::Object.new; @@a' + mutants << '@@srandom = nil; @@a' + end + end + + context 'instance variable' do + let(:source) { '@a = nil; @a' } + + let(:mutations) do + mutants = [] + mutants << '@a = nil; nil' + mutants << '@a = nil' + mutants << '@a' + mutants << '@a = ::Object.new; @a' + mutants << '@srandom = nil; @a' + end + + it_should_behave_like 'a mutator' + end + + context 'local variable' do + let(:source) { 'a = nil; a' } + + let(:mutations) do + mutants = [] + mutants << 'a = nil; nil' + mutants << 'a = nil' + mutants << 'a' + mutants << 'a = ::Object.new; a' + mutants << 'srandom = nil; a' + end + + it_should_behave_like 'a mutator' + end + + context 'self' do + let(:source) { 'self' } + + let(:mutations) do + mutants = [] + mutants << 'nil' + end + + it_should_behave_like 'a mutator' + end +end diff --git a/spec/unit/mutant/mutator/node/named_value/constant_assignment/mutation_spec.rb b/spec/unit/mutant/mutator/node/named_value/constant_assignment/mutation_spec.rb new file mode 100644 index 00000000..ea693398 --- /dev/null +++ b/spec/unit/mutant/mutator/node/named_value/constant_assignment/mutation_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Mutant::Mutator::Node::NamedValue::VariableAssignment, 'mutations' do + + before do + Mutant::Random.stub(:hex_string => :random) + end + + let(:source) { 'A = true' } + + let(:mutations) do + mutations = [] + + mutations << 'SRANDOM = true' + mutations << 'A = false' + mutations << 'A = nil' + end + + it_should_behave_like 'a mutator' +end diff --git a/spec/unit/mutant/mutator/node/assignment/mutation_spec.rb b/spec/unit/mutant/mutator/node/named_value/variable_assignment/mutation_spec.rb similarity index 93% rename from spec/unit/mutant/mutator/node/assignment/mutation_spec.rb rename to spec/unit/mutant/mutator/node/named_value/variable_assignment/mutation_spec.rb index d20220f3..59449390 100644 --- a/spec/unit/mutant/mutator/node/assignment/mutation_spec.rb +++ b/spec/unit/mutant/mutator/node/named_value/variable_assignment/mutation_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Mutant::Mutator::Node::Assignment, 'mutations' do +describe Mutant::Mutator::Node::NamedValue::VariableAssignment, 'mutations' do before do Mutant::Random.stub(:hex_string => :random) diff --git a/spec/unit/mutant/mutator/node/send/mutation_spec.rb b/spec/unit/mutant/mutator/node/send/mutation_spec.rb index c53b3bad..9d40939d 100644 --- a/spec/unit/mutant/mutator/node/send/mutation_spec.rb +++ b/spec/unit/mutant/mutator/node/send/mutation_spec.rb @@ -9,6 +9,7 @@ describe Mutant::Mutator, 'send' do let(:mutations) do mutations = [] mutations << 'foo ||= expression' + mutations << 'nil.foo ||= expression' end it_should_behave_like 'a mutator' @@ -74,6 +75,7 @@ describe Mutant::Mutator, 'send' do mutations = [] mutations << 'foo' mutations << 'self' + mutations << 'nil.foo' end it_should_behave_like 'a mutator' @@ -111,6 +113,7 @@ describe Mutant::Mutator, 'send' do mutations = [] mutations << 'self.class' mutations << 'self.foo' + mutations << 'nil.class.foo' end it_should_behave_like 'a mutator' @@ -142,6 +145,7 @@ describe Mutant::Mutator, 'send' do mutations << 'foo(nil)' mutations << 'nil' mutations << 'self.foo(::Object.new)' + mutations << 'nil.foo(nil)' end it_should_behave_like 'a mutator' @@ -187,6 +191,8 @@ describe Mutant::Mutator, 'send' do mutations = [] mutations << 'foo' mutations << 'left - right' + mutations << 'left / foo' + mutations << 'right / foo' end it_should_behave_like 'a mutator' diff --git a/spec/unit/mutant/mutator/node/super/mutation_spec.rb b/spec/unit/mutant/mutator/node/super/mutation_spec.rb index dab7cace..0f3c9ed7 100644 --- a/spec/unit/mutant/mutator/node/super/mutation_spec.rb +++ b/spec/unit/mutant/mutator/node/super/mutation_spec.rb @@ -5,7 +5,12 @@ describe Mutant::Mutator, 'super' do context 'with no arguments' do let(:source) { 'super' } - it_should_behave_like 'a noop mutator' + let(:mutations) do + mutations = [] + mutations << 'nil' + end + + it_should_behave_like 'a mutator' end context 'with explicit empty arguments' do diff --git a/spec/unit/mutant/mutator/self_spec.rb b/spec/unit/mutant/mutator/self_spec.rb deleted file mode 100644 index 85aa32c3..00000000 --- a/spec/unit/mutant/mutator/self_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'spec_helper' - -describe Mutant::Mutator, 'self' do - let(:source) { 'self' } - - it_should_behave_like 'a noop mutator' -end diff --git a/spec/unit/mutant/node_helpers/n_not_spec.rb b/spec/unit/mutant/node_helpers/n_not_spec.rb new file mode 100644 index 00000000..6e737e27 --- /dev/null +++ b/spec/unit/mutant/node_helpers/n_not_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Mutant::NodeHelpers, '#n_not' do + subject { object.n_not(node) } + + let(:object) { Object.new.extend(described_class) } + let(:node) { described_class::N_TRUE } + + it 'returns the negated node' do + expect(subject).to eq(parse('not true')) + end +end diff --git a/test_app/spec/shared/mutator_behavior.rb b/test_app/spec/shared/mutator_behavior.rb index 8f2dce80..75337730 100644 --- a/test_app/spec/shared/mutator_behavior.rb +++ b/test_app/spec/shared/mutator_behavior.rb @@ -30,7 +30,10 @@ shared_examples_for 'a mutator' do unless subject == expected_mutations message = "Missing mutations: %s\nUnexpected mutations: %s" % - [expected_mutations - subject, subject - expected_mutations ].map(&:to_a).map(&:inspect) + [ + expected_mutations - subject, + subject - expected_mutations + ].map(&:to_a).map(&:inspect) fail message end end diff --git a/test_app/spec/spec_helper.rb b/test_app/spec/spec_helper.rb index 043aad0f..e52d0c4d 100644 --- a/test_app/spec/spec_helper.rb +++ b/test_app/spec/spec_helper.rb @@ -6,4 +6,6 @@ require 'test_app' require 'rspec' # require spec support files and shared behavior -Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each { |f| require f } +Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each do |file| + require file +end diff --git a/test_app/spec/unit/test_app/literal/string_spec.rb b/test_app/spec/unit/test_app/literal/string_spec.rb index 6ef6ccb6..8704f1e7 100644 --- a/test_app/spec/unit/test_app/literal/string_spec.rb +++ b/test_app/spec/unit/test_app/literal/string_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe TestApp::Literal,'#string' do +describe TestApp::Literal, '#string' do subject { object.string } let(:object) { described_class.new }