Merge branch 'development'

Force myself to fix this branch...

Conflicts:
	mutant.gemspec
This commit is contained in:
Markus Schirp 2013-02-21 22:22:53 +01:00
commit f27c686d35
86 changed files with 2058 additions and 2343 deletions

View file

@ -2,5 +2,7 @@ source 'https://rubygems.org'
gemspec
gem 'composition', :git => 'https://github.com/mbj/composition.git'
gem 'devtools', :git => 'https://github.com/datamapper/devtools.git'
eval(File.read(File.join(File.dirname(__FILE__),'Gemfile.devtools')))

View file

@ -21,7 +21,7 @@ group :guard do
gem 'rb-inotify', '~> 0.9.0', :require => false
# Remove this one https://github.com/guard/listen/pull/78 is released
gem 'listen', '~> 0.7.2', :git => 'https://github.com/guard/listen'
gem 'listen', '~> 0.7.2', :git => 'https://github.com/guard/listen.git'
# notification handling
gem 'libnotify', '~> 0.8.0', :require => false
@ -34,17 +34,13 @@ group :metrics do
gem 'flog', '~> 2.5.3'
gem 'reek', '~> 1.2.13', :git => 'https://github.com/troessner/reek.git', :ref => 'ef77fcecaa21c9ebcbe4d9a79d41b0e70196bf18'
gem 'roodi', '~> 2.1.0'
gem 'yardstick', '~> 0.9.0'
gem 'yardstick', '~> 0.9.1'
platforms :ruby_18, :ruby_19 do
# this indirectly depends on ffi which does not build on ruby-head
gem 'yard-spellcheck', '~> 0.1.5'
end
platforms :mri_18 do
gem 'rcov', '~> 1.0.0'
end
platforms :mri_19 do
gem 'simplecov', '~> 0.7.1'
end

View file

@ -3,7 +3,7 @@ mutant
[![Build Status](https://secure.travis-ci.org/mbj/mutant.png?branch=master)](http://travis-ci.org/mbj/mutant)
[![Dependency Status](https://gemnasium.com/mbj/mutant.png)](https://gemnasium.com/mbj/mutant)
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/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 that aims to be better than existing mutation testers.

View file

@ -10,6 +10,7 @@ require 'digest/sha1'
require 'inflecto'
require 'to_source'
require 'ice_nine'
require 'composition'
require 'diff/lcs'
require 'diff/lcs/hunk'
require 'rspec'
@ -38,6 +39,8 @@ require 'mutant/helper'
require 'mutant/random'
require 'mutant/mutator'
require 'mutant/mutation'
require 'mutant/mutation/evil'
require 'mutant/mutation/neutral'
require 'mutant/mutation/filter'
require 'mutant/mutation/filter/code'
require 'mutant/mutation/filter/whitelist'
@ -84,23 +87,35 @@ require 'mutant/loader'
require 'mutant/context'
require 'mutant/context/scope'
require 'mutant/subject'
require 'mutant/subject/method'
require 'mutant/matcher'
require 'mutant/matcher/chain'
require 'mutant/matcher/object_space'
require 'mutant/matcher/method'
require 'mutant/matcher/method/singleton'
require 'mutant/matcher/method/instance'
require 'mutant/matcher/scope_methods'
require 'mutant/matcher/method/classifier'
require 'mutant/matcher/methods'
require 'mutant/matcher/namespace'
require 'mutant/killer'
require 'mutant/killer/static'
require 'mutant/killer/rspec'
require 'mutant/killer/forking'
require 'mutant/killer/forked'
require 'mutant/strategy'
require 'mutant/strategy/static'
require 'mutant/strategy/method_expansion'
require 'mutant/strategy/rspec'
require 'mutant/strategy/rspec/example_lookup'
require 'mutant/strategy/rspec/dm2'
require 'mutant/strategy/rspec/dm2/lookup'
require 'mutant/strategy/rspec/dm2/lookup/method'
require 'mutant/runner'
require 'mutant/runner/config'
require 'mutant/runner/subject'
require 'mutant/runner/mutation'
require 'mutant/cli_parser'
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/reporter'

View file

@ -1,14 +1,9 @@
module Mutant
# Comandline parser
class CLI
class CLI < CLIParser
include Adamantium::Flat, Equalizer.new(:matcher, :filter, :strategy, :reporter)
# Error raised when CLI argv is inalid
Error = Class.new(RuntimeError)
EXIT_FAILURE = 1
EXIT_SUCCESS = 0
# Run cli with arguments
#
# @param [Array<String>] arguments
@ -19,30 +14,37 @@ module Mutant
# @api private
#
def self.run(*arguments)
error = Runner.run(new(*arguments)).fail?
error ? EXIT_FAILURE : EXIT_SUCCESS
config = new(*arguments)
runner = Runner::Config.run(config)
runner.success? ? EXIT_SUCCESS : EXIT_FAILURE
rescue Error => exception
$stderr.puts(exception.message)
EXIT_FAILURE
end
# Return matcher
OPTIONS = {
'--code' => [:add_filter, Mutation::Filter::Code ],
'--debug' => [:set_debug ],
'-d' => [:set_debug ],
'--rspec-unit' => [:set_strategy, Strategy::Rspec::Unit ],
'--rspec-full' => [:set_strategy, Strategy::Rspec::Full ],
'--rspec-dm2' => [:set_strategy, Strategy::Rspec::DM2 ]
}.freeze
# Initialize objecct
#
# @return [Mutant::Matcher]
# @param [Array<String>]
#
# @raise [CLI::Error]
# raises error when matcher is not given
# @return [undefined]
#
# @api private
#
def matcher
if @matchers.empty?
raise Error, 'No matchers given'
end
Mutant::Matcher::Chain.build(@matchers)
def initialize(arguments)
@filters, @matchers = [], []
super(arguments)
strategy
matcher
end
memoize :matcher
# Test for running in debug mode
#
@ -80,7 +82,7 @@ module Mutant
# @api private
#
def strategy
@strategy || raise(Error, 'no strategy was set!')
@strategy || raise(Error, 'No strategy was set!')
@strategy.new(self)
end
memoize :strategy
@ -98,94 +100,23 @@ module Mutant
private
OPTIONS = {
'--code' => [:add_filter, Mutation::Filter::Code ],
'-I' => [:add_load_path ],
'--include' => [:add_load_path ],
'-r' => [:require_library ],
'--require' => [:require_library ],
'--debug' => [:set_debug ],
'-d' => [:set_debug ],
'--rspec-unit' => [:set_strategy, Strategy::Rspec::Unit ],
'--rspec-full' => [:set_strategy, Strategy::Rspec::Full ],
'--rspec-dm2' => [:set_strategy, Strategy::Rspec::DM2 ],
'--static-fail' => [:set_strategy, Strategy::Static::Fail ],
'--static-success' => [:set_strategy, Strategy::Static::Success ]
}.freeze
OPTION_PATTERN = %r(\A-(?:-)?[a-zA-Z0-9\-]+\z).freeze
# Initialize CLI
# Return matcher
#
# @param [Array<String>] arguments
#
# @return [undefined]
#
# @api private
#
def initialize(arguments)
@filters, @matchers = [], []
@arguments, @index = arguments, 0
while @index < @arguments.length
dispatch
end
strategy
matcher
end
# Return option for argument with index
#
# @param [Fixnum] index
#
# @return [String]
#
# @api private
#
def option(index)
@arguments.fetch(index+1)
end
# Return current argument
#
# @return [String]
#
# @api private
#
def current_argument
@arguments.fetch(@index)
end
# Return current option value
#
# @return [String]
# @return [Mutant::Matcher]
#
# @raise [CLI::Error]
# raises error when option is missing
# raises error when matcher is not given
#
# @api private
#
def current_option_value
@arguments.fetch(@index+1)
rescue IndexError
raise Error, "#{current_argument.inspect} is missing an argument"
end
# Process current argument
#
# @return [undefined]
#
# @api private
#
def dispatch
if OPTION_PATTERN =~ current_argument
dispatch_option
else
dispatch_matcher
def matcher
if @matchers.empty?
raise Error, 'No matchers given'
end
Mutant::Matcher::Chain.build(@matchers)
end
memoize :matcher
# Move processed argument by amount
#
@ -208,15 +139,8 @@ module Mutant
#
def dispatch_matcher
argument = current_argument
matcher = Mutant::Matcher.from_string(argument)
unless matcher
raise Error, "Invalid matcher syntax: #{argument.inspect}"
end
@matchers << matcher
consume(1)
@matchers << Classifier.build(argument)
end
# Process option argument
@ -246,30 +170,6 @@ module Mutant
consume(2)
end
# Add load path
#
# @api private
#
# @return [undefined]
#
def add_load_path
$LOAD_PATH << current_option_value
consume(2)
end
# Enable rspec
#
# @api private
#
# @return [self]
#
# @api private
#
def enable_rspec
consume(1)
@rspec = true
end
# Set debug mode
#
# @api private
@ -293,16 +193,5 @@ module Mutant
consume(1)
@strategy = strategy
end
# Require library
#
# @api private
#
# @return [undefined]
#
def require_library
require(current_option_value)
consume(2)
end
end
end

View file

@ -0,0 +1,129 @@
module Mutant
class CLI
# A classifier for input strings
class Classifier < Matcher
include AbstractType, Adamantium::Flat, Equalizer.new(:identification)
extend DescendantsTracker
SCOPE_NAME_PATTERN = /[A-Za-z][A-Za-z_0-9]*/.freeze
METHOD_NAME_PATTERN = /[_A-Za-z][A-Za-z0-9_]*[!?=]?/.freeze
SCOPE_PATTERN = /(?:::)?#{SCOPE_NAME_PATTERN}(?:::#{SCOPE_NAME_PATTERN})*/.freeze
SINGLETON_PATTERN = %r(\A(#{SCOPE_PATTERN})\z).freeze
# Return constant
#
# @param [String] location
#
# @return [Class|Module]
#
# @api private
#
def self.constant_lookup(location)
location.gsub(%r(\A::), '').split('::').inject(::Object) do |parent, name|
parent.const_get(name)
end
end
# Return matchers for input
#
# @param [String] input
#
# @return [Classifier]
# if a classifier handles the input
#
# @return [nil]
# otherwise
#
# @api private
#
def self.build(input)
classifiers = descendants.map do |descendant|
descendant.run(input)
end.compact
raise if classifiers.length > 1
classifiers.first
end
# Run classifier
#
# @return [Classifier]
# if input is handled by classifier
#
# @return [nil]
# otherwise
#
# @api private
#
def self.run(input)
match = self::REGEXP.match(input)
return unless match
new(match)
end
# No protected_class_method in ruby :(
class << self; protected :run; end
# Enumerate subjects
#
# @return [self]
# if block given
#
# @return [Enumerator<Subject>]
# otherwise
#
# @api private
#
def each(&block)
return to_enum unless block_given?
matcher.each(&block)
self
end
# Return identification
#
# @return [String]
#
# @api private
#
def identification
match.to_s
end
memoize :identification
private
# Initialize object
#
# @param [MatchData] match
#
# @return [undefined]
#
# @api private
#
def initialize(match)
@match = match
end
# Return match
#
# @return [MatchData]
#
# @api private
#
attr_reader :match
# Return matcher
#
# @return [Matcher]
#
# @api private
#
abstract_method :matcher
end
end
end

View file

@ -1,42 +1,20 @@
module Mutant
class Matcher
class Method < self
# A classifier for input strings
class Classifier
include Adamantium::Flat
class CLI
class Classifier
# Explicit method classifier
class Method < self
TABLE = {
'.' => Matcher::ScopeMethods::Singleton,
'#' => Matcher::ScopeMethods::Instance
'.' => Matcher::Methods::Singleton,
'#' => Matcher::Methods::Instance
}.freeze
SCOPE_FORMAT = /\A([^#.]+)(\.|#)(.+)\z/.freeze
REGEXP = %r(\A(#{SCOPE_PATTERN})([.#])(#{METHOD_NAME_PATTERN}\z)).freeze
# Positions of captured regexp groups
# Freezing fixnums to avoid their singleton classes are patched.
SCOPE_NAME_POSITION = 1.freeze
SCOPE_SYMBOL_POSITION = 2.freeze
METHOD_NAME_POSITION = 3.freeze
private_class_method :new
# Run classifier
#
# @param [String] input
#
# @return [Matcher::Method]
# returns matcher when input is in
#
# @return [nil]
# returns nil otherwise
#
# @api private
#
def self.run(input)
match = SCOPE_FORMAT.match(input)
return unless match
new(match).matcher
end
SCOPE_NAME_POSITION = 1
SCOPE_SYMBOL_POSITION = 2
METHOD_NAME_POSITION = 3
# Return method matcher
#
@ -45,10 +23,13 @@ module Mutant
# @api private
#
def matcher
# TODO: Honor law of demeter
scope_matcher.matcher.new(scope, method)
end
memoize :matcher
private
# Return method
#
# @return [Method, UnboundMethod]
@ -56,45 +37,13 @@ module Mutant
# @api private
#
def method
# TODO: Honor law of demeter
scope_matcher.methods.detect do |method|
method.name == method_name
end || raise("Cannot find #{method_name} for #{scope}")
end
memoize :method, :freezer => :noop
# Return match
#
# @return [Matche]
#
# @api private
#
attr_reader :match
private
# Initialize matcher
#
# @param [MatchData] match
#
# @api private
#
def initialize(match)
@match = match
end
# Return scope
#
# @return [Class|Module]
#
# @api private
#
def scope
scope_name.gsub(%r(\A::), '').split('::').inject(::Object) do |parent, name|
parent.const_get(name)
end
end
memoize :scope
# Return scope name
#
# @return [String]
@ -105,6 +54,16 @@ module Mutant
match[SCOPE_NAME_POSITION]
end
# Return scope
#
# @return [Class, Method]
#
# @api private
#
def scope
Classifier.constant_lookup(scope_name)
end
# Return method name
#
# @return [String]
@ -135,6 +94,7 @@ module Mutant
TABLE.fetch(scope_symbol).new(scope)
end
memoize :scope_matcher
end
end
end

View file

@ -0,0 +1,34 @@
module Mutant
class CLI
class Classifier
# Namespace classifier
class Namespace < self
REGEXP = %r(\A(#{SCOPE_PATTERN})\*\z).freeze
private
# Return matcher
#
# @return [Matcher]
#
# @api private
#
def matcher
Matcher::Namespace.new(namespace)
end
# Return namespace
#
# @return [Class, Module]
#
# @api private
#
def namespace
Classifier.const_lookup(match.to_s)
end
end
end
end
end

87
lib/mutant/cli_parser.rb Normal file
View file

@ -0,0 +1,87 @@
module Mutant
# Base class for cli parsers
#
# I hate base classes for reusable functionallity.
# But could not come up with a nice composition/instantiation
# solution.
#
class CLIParser
# Error raised when CLI argv is inalid
Error = Class.new(RuntimeError)
EXIT_FAILURE = 1
EXIT_SUCCESS = 0
OPTION_PATTERN = %r(\A-(?:-)?[a-zA-Z0-9\-]+\z).freeze
# Initialize CLI
#
# @param [Array<String>] arguments
#
# @return [undefined]
#
# @api private
#
def initialize(arguments)
@arguments, @index = arguments, 0
while @index < @arguments.length
dispatch
end
end
private
# Return option for argument with index
#
# @param [Fixnum] index
#
# @return [String]
#
# @api private
#
def option(index)
@arguments.fetch(index+1)
end
# Return current argument
#
# @return [String]
#
# @api private
#
def current_argument
@arguments.fetch(@index)
end
# Return current option value
#
# @return [String]
#
# @raise [CLI::Error]
# raises error when option is missing
#
# @api private
#
def current_option_value
@arguments.fetch(@index+1)
rescue IndexError
raise Error, "#{current_argument.inspect} is missing an argument"
end
# Process current argument
#
# @return [undefined]
#
# @api private
#
def dispatch
if OPTION_PATTERN =~ current_argument
dispatch_option
else
dispatch_matcher
end
end
end
end

View file

@ -10,6 +10,12 @@ module Mutant
undef unless until when while yield
).map(&:to_sym).to_set.freeze
METHOD_NAME_EXPANSIONS = {
/\?\z/ => '_predicate',
/=\z/ => '_writer',
/!\z/ => '_bang'
}.freeze
BINARY_METHOD_OPERATOR_EXPANSIONS = {
:<=> => :spaceship_operator,
:=== => :case_equality_operator,

View file

@ -5,7 +5,9 @@ module Mutant
# Return root ast node
#
# @return [Rubinis::AST::Script]
# @param [Rubnius::AST::Node] node
#
# @return [Rubinis::AST::Node]
#
# @api private
#
@ -19,6 +21,14 @@ module Mutant
#
attr_reader :source_path
# Return identification
#
# @return [String]
#
# @api private
#
abstract_method :identification
private
# Initialize context

View file

@ -16,6 +16,16 @@ module Mutant
end
end
# Return identification
#
# @return [String]
#
# @ai private
#
def identification
scope.name
end
# Wrap node into ast node
#
# @param [Class, Module] scope
@ -66,6 +76,16 @@ module Mutant
name_nesting.last
end
# Return name
#
# @return [String]
#
# @api private
#
def name
scope.name
end
# Return scope wrapped by context
#
# @return [::Module|::Class]
@ -88,26 +108,6 @@ module Mutant
@scope = scope
end
# Return scope AST class
#
# @return [Rubinius::AST::Node]
#
# @api private
#
def scope_class
self.class::SCOPE_CLASS
end
# Return keyword
#
# @return [Rubinius::AST::Node]
#
# @api private
#
def keyword
self.class::KEYWORD
end
# Return new root ast
#
# @return [Rubinius::AST::Node]
@ -115,17 +115,7 @@ module Mutant
# @api private
#
def root_ast
"#{keyword} #{qualified_name}; end".to_ast
end
# Return qualified name of scope
#
# @return [String]
#
# @api private
#
def qualified_name
scope.name
"#{keyword} #{name}; end".to_ast
end
# Return nesting of names of scope

View file

@ -19,20 +19,5 @@ module Mutant
Marshal.load(Marshal.dump(object))
end
# Extract option from options hash
#
# @param [Hash] options
# @param [Object] key
#
# @return [Object] value
#
# @api private
#
def self.extract_option(options, key)
options.fetch(key) do
raise ArgumentError,"Missing #{key.inspect} in options"
end
end
end
end

View file

@ -1,20 +1,65 @@
module Mutant
# Abstract base class for mutant killers
class Killer
include Adamantium::Flat, AbstractType
include Adamantium::Flat, AbstractType, Equalizer.new(:strategy, :mutation, :killed?)
# Return strategy
#
# @return [Strategy]
#
# @api private
#
attr_reader :strategy
# 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
run_with_benchmark
end
# Test for kill failure
#
# @return [true]
# returns true when mutant was killed
# when killer succeeded
#
# @return [false]
# returns false otherwise
# otherwise
#
# @api private
#
def fail?
!@killed
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 runtime of killer
@ -25,16 +70,6 @@ module Mutant
#
attr_reader :runtime
# Return original source
#
# @return [String]
#
# @api private
#
def original_source
mutation.original_source
end
# Return mutated source
#
# @return [String]
@ -45,69 +80,8 @@ module Mutant
mutation.source
end
# Return strategy
#
# @return [Strategy]
#
# @api private
#
attr_reader :strategy
# Return name of killer
#
# @return [String]
#
# @api private
#
def self.type
self::TYPE
end
# Return identification
#
# @return [String]
#
# @api private
#
def identification
"#{type}:#{mutation.identification}".freeze
end
memoize :identification
# Return mae of killer
#
# @return [String]
#
# @api private
#
def type
self.class.type
end
# Return mutation to kill
#
# @return [Mutation]
#
# @api private
#
attr_reader :mutation
private
# Initialize killer object
#
# @param [Mutation] mutation
#
# @return [undefined]
#
# @api private
#
def initialize(strategy, mutation)
@strategy, @mutation = strategy, mutation
run_with_benchmark
end
# Run with taking the time
#
# @return [undefined]
@ -121,13 +95,13 @@ module Mutant
@runtime = end_time - start_time
end
# Run test
# Run killer
#
# @return [true]
# returns true when mutant was killed
# when mutant was killed
#
# @return [false]
# returns false otherwise
# otherwise
#
# @api private
#

View file

@ -0,0 +1,48 @@
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
fork do
begin
killer = @killer.new(strategy, mutation)
Kernel.exit(killer.fail? ? 1 : 0)
rescue
Kernel.exit(1)
end
end
status = Process.wait2.last
status.exitstatus.zero?
end
end
end
end

View file

@ -1,59 +1,6 @@
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
# Return killer type
#
# @return [String]
#
# @api private
#
def type
@killer.type
end
private
# Run killer
#
# @return [true]
# if mutant was killed
#
# @return [false]
# otherwise
#
# @api private
#
def run
fork do
begin
killer = @killer.new(strategy, mutation)
Kernel.exit(killer.fail? ? 1 : 0)
rescue
Kernel.exit(1)
end
end
status = Process.wait2.last
status.exitstatus.zero?
end
end
# A killer that executes other killer in forked environemnts
class Forking < self
include Equalizer.new(:killer)
@ -93,6 +40,5 @@ module Mutant
end
end
end
end

View file

@ -2,36 +2,23 @@ module Mutant
class Killer
# Runner for rspec tests
class Rspec < self
TYPE = 'rspec'.freeze
private
# Initialize rspec runner
#
# @return [undefined]
#
# @api private
#
def initialize(*)
@error_stream, @output_stream = StringIO.new, StringIO.new
super
end
# Run rspec test
#
# @return [true]
# returns true when test is NOT successful and the mutant was killed
# when test is NOT successful
#
# @return [false]
# returns false otherwise
# otherwise
#
# @api private
#
def run
mutation.insert
!::RSpec::Core::Runner.run(command_line_arguments, strategy.error_stream, strategy.output_stream).zero?
!!::RSpec::Core::Runner.run(command_line_arguments, strategy.error_stream, strategy.output_stream).nonzero?
end
memoize :run
# Return command line arguments
#
@ -42,8 +29,9 @@ module Mutant
def command_line_arguments
%W(
--fail-fast
) + strategy.spec_files(mutation)
) + strategy.spec_files(mutation.subject)
end
end
end
end

View file

@ -19,13 +19,11 @@ module Mutant
# Killer that is always successful
class Success < self
TYPE = 'success'.freeze
RESULT = true
end
# Killer that always fails
class Fail < self
TYPE = 'fail'.freeze
RESULT = false
end
end

View file

@ -1,14 +1,37 @@
module Mutant
# Abstract matcher to find ASTs to mutate
# Abstract matcher to find subjects to mutate
class Matcher
include Adamantium::Flat, Enumerable, AbstractType
extend DescendantsTracker
# Enumerate subjects
#
# @param [Object] input
#
# @return [self]
# if block given
#
# @return [Enumerator<Subject>]
#
# @api private
#
# @return [undefined]
def self.each(input, &block)
return to_enum(__method__, input) unless block_given?
new(input).each(&block)
self
end
# Enumerate subjects
#
# @api private
#
# @return [self]
# if block given
#
# @return [Enumerabe<Subject>]
# otherwise
#
abstract_method :each
@ -19,39 +42,5 @@ module Mutant
# @api private
#
abstract_method :identification
# Return matcher
#
# @param [String] input
#
# @return [nil]
# returns nil as default implementation
#
# @api private
#
def self.parse(input)
nil
end
# Return match from string
#
# @param [String] input
#
# @return [Matcher]
# returns matcher input if successful
#
# @return [nil]
# returns nil otherwise
#
# @api private
#
def self.from_string(input)
descendants.each do |descendant|
matcher = descendant.parse(input)
return matcher if matcher
end
nil
end
end
end

View file

@ -4,18 +4,6 @@ module Mutant
class Method < self
include Adamantium::Flat, Equalizer.new(:identification)
# Parse a method string into filter
#
# @param [String] input
#
# @return [Matcher::Method]
#
# @api private
#
def self.parse(input)
Classifier.run(input)
end
# Methods within rbx kernel directory are precompiled and their source
# cannot be accessed via reading source location
BLACKLIST = /\Akernel\//.freeze
@ -35,9 +23,8 @@ module Mutant
return self if skip?
subject.tap do |subject|
yield subject if subject
end
util = subject
yield util if util
self
end
@ -68,18 +55,6 @@ module Mutant
method.name
end
# Test if method is public
#
# @return [true]
# if method is public
#
# @return [false]
# otherwise
#
# @api private
#
abstract_method :public?
private
# Initialize method filter
@ -93,8 +68,6 @@ module Mutant
#
def initialize(scope, method)
@scope, @method = scope, method
# FIXME: cache public private should not be needed, loader should not override visibility! (But does currently) :(
public?
end
# Test if method is skipped
@ -180,7 +153,7 @@ module Mutant
def subject
node = matched_node
return unless node
Subject.new(self, context, node)
self.class::SUBJECT_CLASS.new(context, node)
end
memoize :subject

View file

@ -1,9 +1,9 @@
module Mutant
class Matcher
class Method < self
class Method
# Matcher for instance methods
class Instance < self
SUBJECT_CLASS = Subject::Method::Instance
# Return identification
#
@ -14,21 +14,7 @@ module Mutant
def identification
"#{scope.name}##{method_name}"
end
# Test if method is public
#
# @return [true]
# if method is public
#
# @return [false]
# otherwise
#
# @api private
#
def public?
scope.public_method_defined?(method_name)
end
memoize :public?
memoize :identification
private
@ -45,7 +31,7 @@ module Mutant
# @api private
#
def match?(node)
node.line == source_line &&
node.line == source_line &&
node.class == Rubinius::AST::Define &&
node.name == method_name
end

View file

@ -3,6 +3,7 @@ module Mutant
class Method
# Matcher for singleton methods
class Singleton < self
SUBJECT_CLASS = Subject::Method::Singleton
# Return identification
#
@ -15,22 +16,6 @@ module Mutant
end
memoize :identification
# Test if method is public
#
# @return [true]
# if method is public
#
# @return [false]
# otherwise
#
# @api private
#
def public?
scope.singleton_class.public_method_defined?(method_name)
end
memoize :public?
private
# Test for node match
@ -104,7 +89,7 @@ module Mutant
when Rubinius::AST::ConstantAccess
receiver_name?(receiver)
else
$stderr.puts "Unable to find singleton method definition only match receiver on Rubinius::AST::Self or Rubinius::AST::ConstantAccess, got #{receiver.class}"
$stderr.puts "Unable to find singleton method definition can only match receiver on Rubinius::AST::Self or Rubinius::AST::ConstantAccess, got #{receiver.class}"
false
end
end

View file

@ -1,17 +1,29 @@
module Mutant
class Matcher
# Abstract base class for matcher that returns subjects extracted from scope methods
class ScopeMethods < self
include AbstractType
# Abstract base class for matcher that returns method subjects extracted from scope
class Methods < self
include AbstractType, Equalizer.new(:scope)
# Return scope
#
# @return [Class,Model]
# @return [Class, Model]
#
# @api private
#
attr_reader :scope
# Initialize object
#
# @param [Class,Module] scope
#
# @return [undefined]
#
# @api private
#
def initialize(scope)
@scope = scope
end
# Enumerate subjects
#
# @return [self]
@ -32,6 +44,16 @@ module Mutant
self
end
# Return method matcher class
#
# @return [Class:Matcher::Method]
#
# @api private
#
def matcher
self.class::MATCHER
end
# Return methods
#
# @return [Enumerable<Method, UnboundMethod>]
@ -45,15 +67,7 @@ module Mutant
end
memoize :methods
# Return method matcher class
#
# @return [Class:Matcher::Method]
#
# @api private
#
def matcher
self.class::MATCHER
end
private
# Return method names
#
@ -69,20 +83,6 @@ module Mutant
object.protected_instance_methods(false)
end
private
# Initialize object
#
# @param [Class,Module] scope
#
# @return [undefined]
#
# @api private
#
def initialize(scope)
@scope = scope
end
# Emit matches for method
#
# @param [UnboundMethod, Method] method

View file

@ -1,37 +1,16 @@
module Mutant
class Matcher
# Matcher against object space
class ObjectSpace < self
include Equalizer.new(:scope_name_pattern)
PATTERN = %r(\A::(.+)\z)
# Parse matcher
#
# @param [String] input
#
# @return [Matcher::ObjectSpace]
# returns object space matcher if successful
#
# @return [nil]
# returns nil otherwise
#
# @api private
#
def self.parse(input)
match = PATTERN.match(input)
return unless match
new(%r(\A#{Regexp.escape(match[1])}(\z|::)))
end
# Matcher for specific namespace
class Namespace < self
include Equalizer.new(:pattern)
# Enumerate subjects
#
# @return [Enumerator<Subject>]
# returns subject enumerator when no block given
#
# @return [self]
# returns self otherwise
# if block given
#
# @return [Enumerator<Subject>]
# otherwise
#
# @api private
#
@ -45,29 +24,41 @@ module Mutant
self
end
# Return scope name pattern
# Return namespace
#
# @return [Regexp]
# @return [Class::Module]
#
# @api private
#
def scope_name_pattern; @scope_name_pattern; end
attr_reader :namespace
MATCHERS = [Matcher::Methods::Singleton, Matcher::Methods::Instance]
private
# Initialize object space matcher
#
# @param [Regexp] scope_name_pattern
# @param [Enumerable<#each(scope)>] matchers
# @param [Class, Module] namespace
#
# @return [undefined]
#
# @api private
#
def initialize(scope_name_pattern, matchers = [Matcher::ScopeMethods::Singleton, Matcher::ScopeMethods::Instance])
@scope_name_pattern, @matchers = scope_name_pattern, @matchers = matchers #[Method::Singleton, Method::Instance]
def initialize(namespace)
@namespace = namespace
end
# Return pattern
#
# @return [Regexp]
#
# @api private
#
def pattern
%r(\A#{Regexp.escape(namespace.name)}(?:::)?\z)
end
memoize :pattern
# Yield matchers for scope
#
# @param [::Class,::Module] scope
@ -77,8 +68,8 @@ module Mutant
# @api private
#
def emit_scope_matches(scope, &block)
@matchers.each do |matcher|
matcher.new(scope).each(&block)
MATCHERS.each do |matcher|
matcher.each(scope, &block)
end
end
@ -91,7 +82,7 @@ module Mutant
def scopes(&block)
return to_enum(__method__) unless block_given?
::ObjectSpace.each_object(Module) do |scope|
::ObjectSpace.each_object(Module).each do |scope|
emit_scope(scope, &block)
end
end
@ -105,7 +96,7 @@ module Mutant
# @api private
#
def emit_scope(scope)
if [::Module, ::Class].include?(scope.class) and @scope_name_pattern =~ scope.name
if pattern =~ scope.name
yield scope
end
end

View file

@ -1,7 +1,20 @@
module Mutant
# Represent a mutated node with its subject
class Mutation
include Adamantium::Flat, Equalizer.new(:sha1)
include AbstractType, Adamantium::Flat, Equalizer.new(:sha1)
# Initialize mutation object
#
# @param [Subject] subject
# @param [Rubinius::Node::AST] node
#
# @return [undefined]
#
# @api private
#
def initialize(subject, node)
@subject, @node = subject, node
end
# Return mutation subject
#
@ -30,6 +43,20 @@ module Mutant
end
memoize :root
# Test if killer is successful
#
# @param [Killer] killer
#
# @return [true]
# if killer is successful
#
# @return [false]
# otherwise
#
# @api private
#
abstract_method :success?
# Insert mutated node
#
# @return [self]
@ -95,46 +122,5 @@ module Mutant
subject.source
end
private
# Initialize mutation object
#
# @param [Subject] subject
# @param [Rubinius::Node::AST] node
#
# @return [undefined]
#
# @api private
#
def initialize(subject, node)
@subject, @node = subject, node
end
# Noop mutation
class Noop < self
# Initialize object
#
# @param [Subject] subject
#
# @return [undefined]
#
# @api private
#
def initialize(subject)
super(subject, subject.node)
end
# Return identification
#
# @return [String]
#
# @api private
#
def identification
"noop:#{super}"
end
memoize :identification
end
end
end

View file

@ -0,0 +1,35 @@
module Mutant
class Mutation
# Evul mutation
class Evil < self
# Return identification
#
# @return [String]
#
# @api private
#
def identification
"evil:#{super}"
end
memoize :identification
# Test if killer is successful
#
# @param [Killer] killer
#
# @return [true]
# if killer killed mutation
#
# @return [false]
# otherwise
#
# @api private
#
def success?(killer)
killer.killed?
end
end
end
end

View file

@ -0,0 +1,35 @@
module Mutant
class Mutation
# Neutral mutation
class Neutral < self
# Return identification
#
# @return [String]
#
# @api private
#
def identification
"noop:#{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
end
end

View file

@ -8,41 +8,48 @@ module Mutant
handle(Rubinius::AST::Self)
# Currently unhandled node classes. Feel free to contribute your mutator!
handle(Rubinius::AST::ZSuper)
handle(Rubinius::AST::ElementAssignment)
handle(Rubinius::AST::AttributeAssignment)
handle(Rubinius::AST::Not)
handle(Rubinius::AST::And)
handle(Rubinius::AST::Or)
handle(Rubinius::AST::Defined)
handle(Rubinius::AST::Super)
handle(Rubinius::AST::Next)
handle(Rubinius::AST::Break)
handle(Rubinius::AST::Match3)
handle(Rubinius::AST::ZSuper)
handle(Rubinius::AST::MultipleAssignment)
handle(Rubinius::AST::ScopedConstant)
handle(Rubinius::AST::LocalVariableAccess)
handle(Rubinius::AST::InstanceVariableAccess)
handle(Rubinius::AST::GlobalVariableAccess)
handle(Rubinius::AST::ClassVariableAccess)
handle(Rubinius::AST::ToplevelConstant)
handle(Rubinius::AST::Ensure)
handle(Rubinius::AST::Rescue)
handle(Rubinius::AST::DynamicString)
handle(Rubinius::AST::DynamicSymbol)
handle(Rubinius::AST::DynamicRegex)
handle(Rubinius::AST::File)
handle(Rubinius::AST::NthRef)
handle(Rubinius::AST::OpAssignOr19)
handle(Rubinius::AST::BlockPass19)
handle(Rubinius::AST::OpAssign1)
handle(Rubinius::AST::OpAssign2)
handle(Rubinius::AST::SplatValue)
handle(Rubinius::AST::ConstantAccess)
handle(Rubinius::AST::Yield)
handle(Rubinius::AST::Begin)
handle(Rubinius::AST::Rescue)
#
# FIXME: This list is mixed with some 1.8 only nodes that should be extracted
#
%w(
ZSuper
ElementAssignment
AttributeAssignment
Not
And
Or
Defined
Super
Next
Break
Match3
ZSuper
MultipleAssignment
ScopedConstant
LocalVariableAccess
InstanceVariableAccess
GlobalVariableAccess
ClassVariableAccess
ToplevelConstant
Ensure
Rescue
DynamicString
DynamicSymbol
DynamicRegex
File
OpAssignOr19
BlockPass19
OpAssign1
NthRef
OpAssign2
SplatValue
ConstantAccess
Yield
Begin
Rescue
).each do |name|
handle(Rubinius::AST.const_get(name))
end
private

View file

@ -1,7 +1,54 @@
module Mutant
# Abstract reporter
class Reporter
include Adamantium::Flat, AbstractType
include Adamantium::Flat, AbstractType, Equalizer.new(:stats)
# Initialize reporter
#
# @param [Config] config
#
# @api private
#
def initialize(config)
@stats = Stats.new
@config = config
end
# Test for success
#
# @return [true]
# if there are subjects and no errors
#
# @return [false]
# otherwise
#
# @api private
#
def success?
stats.success?
end
# Report start
#
# @param [Config] config
#
# @return [self]
#
# @api private
#
def start(_config)
self
end
# Report stop
#
# @return [self]
#
# @api private
#
def stop
self
end
# Report subject
#
@ -11,7 +58,10 @@ module Mutant
#
# @api private
#
abstract_method :subject
def subject(_subject)
stats.count_subject
self
end
# Report mutation
#
@ -21,17 +71,9 @@ module Mutant
#
# @api private
#
abstract_method :mutation
# Report notice
#
# @param [String] notice
#
# @return [self]
#
# @api private
#
abstract_method :notice
def mutation(_mutation)
self
end
# Report killer
#
@ -41,44 +83,54 @@ module Mutant
#
# @api private
#
abstract_method :killer
# Report config
#
# @param [Mutant::Config] config
#
# @return [self]
#
# @api private
#
abstract_method :config
# Return output stream
#
# @return [IO]
#
# @api private
#
abstract_method :output_stream
# Return error stream
#
# @return [IO]
#
# @api private
#
abstract_method :error_stream
private
# Initialize reporter
#
# @param [Config] config
#
# @api private
#
def initialize(config)
@config = config
def report_killer(killer)
stats.count_killer(killer)
self
end
# Test for running in debug mode
#
# @return [true]
# if running in debug mode
#
# @return [false]
# otherwise
#
# @api private
#
def debug?
config.debug?
end
# Return stats
#
# @return [Reporter::Stats]
#
# @api private
#
attr_reader :stats
# Return config
#
# @return [Config]
#
# @api private
#
attr_reader :config
# Test if errors are present
#
# @return [true]
# if errors are present
#
# @return [false]
# otherwise
#
# @api private
#
def errors?
stats.errors?
end
end
end

View file

@ -2,165 +2,6 @@ module Mutant
class Reporter
# Reporter that reports in human readable format
class CLI < self
include Equalizer.new(:io)
# Reporter subject
#
# @param [Subject] subject
#
# @return [self]
#
# @api private
#
def subject(subject)
stats.subject
puts("Subject: #{subject.identification}")
end
# Return error stream
#
# @return [IO]
#
# @api private
#
def error_stream
@config.debug? ? io : StringIO.new
end
# Return output stream
#
# @return [IO]
#
# @api private
#
def output_stream
@config.debug? ? io : StringIO.new
end
# Report mutation
#
# @param [Mutation] mutation
#
# @return [self]
#
# @api private
#
def mutation(mutation)
if @config.debug?
colorized_diff(mutation)
end
self
end
# Report config
#
# @param [Mutant::Config] config
#
# @return [self]
#
# @api private
#
def config(config)
puts 'Mutant configuration:'
puts "Matcher: #{config.matcher.inspect}"
puts "Filter: #{config.filter.inspect}"
puts "Strategy: #{config.strategy.inspect}"
end
# Report noop
#
# @param [Killer] killer
#
# @return [self]
#
# @api private
#
def noop(killer)
color, word =
if killer.fail?
[Color::GREEN, 'Alive']
else
[Color::RED, 'Killed']
end
print_killer(color, word, killer)
unless killer.fail?
puts(killer.mutation.source)
stats.noop_fail(killer)
end
self
end
# Reporter killer
#
# @param [Killer] killer
#
# @return [self]
#
# @api private
#
def killer(killer)
stats.killer(killer)
color, word =
if killer.fail?
[Color::RED, 'Alive']
else
[Color::GREEN, 'Killed']
end
print_killer(color, word, killer)
if killer.fail?
colorized_diff(killer.mutation)
end
self
end
# Report errors
#
# @param [Enumerable<Killer>] errors
#
# @api private
#
# @return [self]
#
def errors(errors)
errors.each do |error|
failure(error)
end
puts
puts "subjects: #{stats.subjects}"
puts "mutations: #{stats.mutations}"
puts "noop_fails: #{stats.noop_fails}"
puts "kills: #{stats.kills}"
puts "alive: #{stats.alive}"
puts "mtime: %02.2fs" % stats.time
puts "rtime: %02.2fs" % stats.runtime
end
# Return IO stream
#
# @return [IO]
#
# @api private
#
attr_reader :io
# Return stats
#
# @return [Stats]
#
# @api private
#
attr_reader :stats
private
# Initialize reporter
#
@ -173,23 +14,120 @@ module Mutant
def initialize(config)
super
@io = $stdout
@stats = Stats.new
end
# Report failure on killer
#
# @param [Killer] killer
# Reporte subject
#
# @return [undefined]
# @param [Subject] subject
#
# @return [self]
#
# @api private
#
def failure(killer)
puts(colorize(Color::RED, "!!! Mutant alive: #{killer.identification} !!!"))
colorized_diff(killer.mutation)
puts("Took: (%02.2fs)" % killer.runtime)
def subject(subject)
super
puts("Subject: #{subject.identification}")
end
# Return error stream
#
# @return [IO]
#
# @api private
#
def error_stream
debug? ? io : StringIO.new
end
# Return output stream
#
# @return [IO]
#
# @api private
#
def output_stream
debug? ? io : StringIO.new
end
# Report mutation
#
# @param [Mutation] mutation
#
# @return [self]
#
# @api private
#
def mutation(mutation)
super
if debug?
colorized_diff(mutation)
end
self
end
# Report start
#
# @param [Mutant::Config] config
#
# @return [self]
#
# @api private
#
def start(config)
message = []
message << 'Mutant configuration:'
message << "Matcher: #{config.matcher.inspect}"
message << "Filter: #{config.filter.inspect}"
message << "Strategy: #{config.strategy.inspect}"
puts message.join("\n")
super
end
# Report stop
#
# @return [self]
#
# @api private
#
def stop
super
end
# Report killer
#
# @param [Killer] killer
#
# @return [self]
#
# @api private
#
def report_killer(killer)
super
status = killer.killed? ? 'Killed' : 'Alive'
color = killer.success? ? Color::GREEN : Color::RED
puts(colorize(color, "%s: %s (%02.2fs)" % [status, killer.identification, killer.runtime]))
unless killer.success?
colorized_diff(killer.mutation)
end
self
end
private
# Return IO stream
#
# @return [IO]
#
# @api private
#
attr_reader :io
# Test for colored output
#
# @return [true]
@ -241,7 +179,7 @@ module Mutant
# @api private
#
def colorized_diff(mutation)
if mutation.kind_of?(Mutation::Noop)
if mutation.kind_of?(Mutation::Neutral)
puts mutation.original_source
return
end
@ -250,29 +188,14 @@ module Mutant
differ = Differ.new(original, current)
diff = color? ? differ.colorized_diff : differ.diff
# FIXME remove this branch before release
if diff.empty?
raise "Unable to create a diff, so ast mutation or to_source has an error!"
raise 'Unable to create a diff, so ast mutation or to_source has an error!'
end
puts(diff)
self
end
# Print killer
#
# @param [Color] color
# @param [String] word
# @param [Killer] killer
#
# @return [undefined]
#
# @api private
#
def print_killer(color, word, killer)
puts(colorize(color, "%s: %s (%02.2fs)" % [word, killer.identification, killer.runtime]))
end
# Test for output to tty
#
# @return [true]
@ -287,6 +210,7 @@ module Mutant
@io.respond_to?(:tty?) && @io.tty?
end
memoize :tty?
end
end
end

View file

@ -4,45 +4,48 @@ module Mutant
# Stats gathered while reporter is running
class Stats
# Return subject count
#
# @return [Fixnum]
#
# @api private
#
attr_reader :subjects
# A counter with fail counts
class Counter
include Equalizer.new(:count, :fails)
# Return mutation count
#
# @return [Fixnum]
#
# @api private
#
attr_reader :mutations
attr_reader :count
# Return skip count
#
# @return [Fixnum]
#
# @api private
#
attr_reader :noop_fails
# Return fail count
#
# @return [Fixnum]
#
# @api private
#
attr_reader :fails
# Return kill count
#
# @return [Fixnum]
#
# @api private
#
attr_reader :kills
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize
@count = @fails = 0
end
# Return mutation runtime
#
# @return [Float]
#
# @api private
#
attr_reader :time
# Count killer
#
# @param [Killer] killer
#
# @return [self]
#
# @api private
#
def handle(killer)
@count += 1
unless killer.success?
@fails += 1
end
self
end
end
include Equalizer.new(:start, :counts, :killers)
# Initialize object
#
@ -51,8 +54,87 @@ module Mutant
# @api private
#
def initialize
@start = Time.now
@noop_fails = @subjects = @mutations = @kills = @time = 0
@start = start
@counts = Hash.new(0)
@killers = {}
end
protected
# Return counts
#
# @return [Hash]
#
# @api private
#
attr_reader :counts
# Return start time
#
# @return [Time]
#
# @api private
#
attr_reader :start
# Return killers
#
# @return [Hash]
#
# @api private
#
attr_reader :killers
public
# Count subject
#
# @return [self]
#
# @api private
#
def count_subject
@counts[:subject] += 1
end
# Test for success?
#
# @return [true]
# if there are subjects and no errors
#
# @return [false]
# otherwise
#
def success?
@counts[:subject].nonzero? && !errors?
end
# Count killer
#
# @param [Killer] killer
#
# @return [self]
#
# @api private
#
def count_killer(killer)
counter = @killers[killer.mutation.class] ||= Counter.new
counter.handle(killer)
self
end
# Test for errors
#
# @return [true]
# if there are errors
#
# @return [false]
# otherwise
#
def errors?
!!@killers.values.inject(0) do |fails, counter|
fails + counter.fails
end.nonzero?
end
# Return runtime in seconds
@ -65,56 +147,6 @@ module Mutant
Time.now - @start
end
# Count subject
#
# @return [self]
#
# @api private
#
def subject
@subjects +=1
self
end
# Return number of mutants alive
#
# @return [Fixnum]
#
# @api private
#
def alive
@mutations - @kills
end
# Count noop mutation fail
#
# @param [Killer] killer
#
# @return [self]
#
# @api private
#
def noop_fail(killer)
@noop_fails += 1
@time += killer.runtime
self
end
# Count killer
#
# @param [Killer] killer
#
# @return [self]
#
# @api private
#
def killer(killer)
@mutations +=1
@kills +=1 unless killer.fail?
@time += killer.runtime
self
end
end
end
end

View file

@ -1,31 +1,9 @@
module Mutant
# Runner that allows to mutate an entire project
class Runner
include Adamantium::Flat
include Adamantium::Flat, AbstractType
extend MethodObject
# Return killers with errors
#
# @return [Enumerable<Killer>]
#
# @api private
#
attr_reader :errors
# Test for failure
#
# @return [true]
# returns true when there are left mutations
#
# @return [false]
# returns false othewise
#
# @api private
#
def fail?
!errors.empty?
end
# Return config
#
# @return [Mutant::Config]
@ -34,8 +12,6 @@ module Mutant
#
attr_reader :config
private
# Initialize object
#
# @param [Config] config
@ -45,106 +21,19 @@ module Mutant
# @api private
#
def initialize(config)
@config, @errors = config, []
util_reporter = reporter
util_reporter.config(config)
@config = config
run
util_reporter.errors(@errors)
end
# Return reporter
#
# @return [Reporter]
#
# @api private
#
def reporter
config.reporter
end
private
# Run mutation killers on subjects
# Perform operation
#
# @return [undefined]
#
# @api private
#
def run
config.matcher.each do |subject|
reporter.subject(subject)
run_subject(subject)
end
end
abstract_method :run
# Run mutation killers on subject
#
# @param [Subject] subject
#
# @return [undefined]
#
# @api private
#
def run_subject(subject)
return unless noop(subject)
subject.each do |mutation|
next unless config.filter.match?(mutation)
reporter.mutation(mutation)
kill(mutation)
end
end
# Test for noop mutation
#
# @param [Subject] subject
#
# @return [true]
# if noop mutation is okay
#
# @return [false]
# otherwise
#
# @api private
#
def noop(subject)
killer = killer(subject.noop)
reporter.noop(killer)
unless killer.fail?
@errors << killer
return false
end
true
end
# Run killer on mutation
#
# @param [Mutation] mutation
#
# @return [true]
# if killer was unsuccessful
#
# @return [false]
# otherwise
#
# @api private
#
def kill(mutation)
killer = killer(mutation)
reporter.killer(killer)
if killer.fail?
@errors << killer
end
end
# Return killer for mutation
#
# @return [Killer]
#
# @api private
#
def killer(mutation)
config.strategy.kill(mutation)
end
end
end

View file

@ -0,0 +1,23 @@
module Mutant
class Runner
class Config < self
# Return subject runners
#
# @return [Enumerable<Runner::Subject>]
#
# @api private
#
attr_reader :subjects
private
def run
@subjects = config.subjects.map do |subject|
Subject.run(config, subject)
end
end
end
end
end

View file

@ -0,0 +1,42 @@
module Mutant
class Runner
# Mutation runner
class Mutation < self
# Return killer instance
#
# @return [Killer]
#
# @api private
#
attr_reader :killer
# Initialize object
#
# @param [Configuration] config
# @param [Mutation] mutation
#
# @return [undefined]
#
# @api private
#
def initialize(config, mutation)
@mutation = mutation
super(config)
end
private
# Perform operation
#
# @return [undefined]
#
# @api private
#
def run
@killer = config.strategy(@mutation).kill(@mutation)
end
end
end
end

View file

@ -0,0 +1,44 @@
module Mutant
class Runner
# Subject specific runner
class Subject < self
# Return mutation runners
#
# @return [Enumerable<Runner::Mutation>]
#
# @api private
#
attr_reader :mutations
# Initialize object
#
# @param [Configuration] config
# @param [Subject] subject
#
# @return [undefined]
#
# @api private
#
def initialize(config, subject)
@subject = subject
super(config)
end
private
# Perform operation
#
# @return [undefined]
#
# @api private
#
def run
@mutations = @subject.map do |mutation|
Mutation.new(config, mutation)
end
end
end
end
end

View file

@ -24,24 +24,24 @@ module Mutant
@config = config
end
# Return output stream
# Perform setup
#
# @return [IO]
# @return [self]
#
# @api private
#
def output_stream
config.reporter.output_stream
def setup
self
end
# Return error stream
# Perform teardown
#
# @return [IO]
# @return [self]
#
# @api private
#
def error_stream
config.reporter.error_stream
def teardown
self
end
# Kill mutation
@ -65,21 +65,5 @@ module Mutant
def killer
self.class::KILLER
end
# Static strategies
class Static < self
include Equalizer.new
# Always fail to kill strategy
class Fail < self
KILLER = Killer::Static::Fail
end
# Always succeed to kill strategy
class Success < self
KILLER = Killer::Static::Success
end
end
end
end

View file

@ -0,0 +1,51 @@
module Mutant
class Strategy
module MethodExpansion
# Run method name expansion
#
# @param [Symbol] name
#
# @return [Symbol]
#
# @api private
#
def self.run(name)
name = map(name) || expand(name)
end
# Return mapped name
#
# @param [Symbol] name
#
# @return [Symbol]
# if name was mapped
#
# @return [nil]
# otherwise
#
# @api private
#
def self.map(name)
OPERATOR_EXPANSIONS[name]
end
private_class_method :map
# Return expanded name
#
# @param [Symbol] name
#
# @return [Symbol]
#
# @api private
#
def self.expand(name)
METHOD_NAME_EXPANSIONS.inject(name.to_s) do |name, find_replace|
name.gsub(*find_replace)
end.to_sym
end
private_class_method :expand
end
end
end

View file

@ -6,18 +6,15 @@ module Mutant
KILLER = Killer::Forking.new(Killer::Rspec)
# DM2-style strategy
class DM2 < self
# Return filename pattern
#
# @return [Enumerable<String>]
#
# @api private
#
def spec_files(mutation)
ExampleLookup.run(mutation)
end
# Setup rspec strategy
#
# @return [self]
#
# @api private
#
def setup
require('./spec/spec_helper.rb')
self
end
# Run all unit specs per mutation
@ -30,7 +27,7 @@ module Mutant
# @api private
#
def spec_files(mutation)
['spec/unit']
Dir['spec/unit/**/*_spec.rb']
end
end

View file

@ -0,0 +1,22 @@
module Mutant
class Strategy
class Rspec
# DM2-style strategy
class DM2 < self
# Return filename pattern
#
# @param [Subject] subject
#
# @return [Enumerable<String>]
#
# @api private
#
def spec_files(subject)
Lookup.run(subject)
end
end
end
end
end

View file

@ -0,0 +1,79 @@
module Mutant
class Strategy
class Rspec
class DM2
# Example lookup for the rspec dm2
class Lookup
include AbstractType, Adamantium::Flat, Equalizer.new(:subject)
# Return subject
#
# @return [Subject]
#
# @api private
#
attr_reader :subject
# Return glob expression
#
# @return [String]
#
# @api private
#
abstract_method :spec_files
# Initalize object
#
# @param [Mutation] mutation
#
# @api private
#
def initialize(subject)
@subject = subject
end
# Perform example lookup
#
# @param [Subject] subject
#
# @return [Enumerable<String>]
#
# @api private
#
def self.run(subject)
build(subject).spec_files
end
REGISTRY = {}
# Register subject hander
#
# @param [Class:Subject]
#
# @return [undefined]
#
# @api private
#
def self.handle(subject_class)
REGISTRY[subject_class]=self
end
private_class_method :handle
# Build lookup object
#
# @param [Subjecŧ] subject
#
# @return [Lookup]
#
# @api private
#
def self.build(subject)
REGISTRY.fetch(subject.class).new(subject)
end
end
end
end
end
end

View file

@ -0,0 +1,135 @@
module Mutant
class Strategy
class Rspec
class DM2
class Lookup
class Method < self
# Return spec files
#
# @return [Enumerable<String>]
#
# @api private
#
def spec_files
Dir.glob(glob_expression)
end
memoize :spec_files
private
# Return base path
#
# @return [String]
#
# @api private
#
def base_path
"spec/unit/#{Inflecto.underscore(subject.context.name)}"
end
# Return method name
#
# @return [Symbol]
#
# @api private
#
def method_name
subject.name
end
# Test if method is public
#
# @return [true]
# if method is public
#
# @return [false]
#
# @api private
#
def public?
subject.public?
end
# Return expanded name
#
# @return [String]
#
# @api private
#
def expanded_name
MethodExpansion.run(method_name)
end
# Return glob expression
#
# @return [String]
#
# @api private
#
def glob_expression
public? ? public_glob_expression : private_glob_expression
end
# Return public glob expression
#
# @return [String]
#
# @api private
#
def public_glob_expression
"#{base_path}/#{expanded_name}_spec.rb"
end
# Return private glob expression
#
# @return [String]
#
# @api private
#
def private_glob_expression
"#{base_path}/*_spec.rb"
end
class Instance < self
handle(Subject::Method::Instance)
private
# Return glob expression
#
# @return [String]
#
# @api private
#
def glob_expression
if method_name == :initialize and !public?
return "#{private_glob_expression} #{base_path}/class_methods/new_spec.rb"
end
super
end
end
class Singleton < self
handle(Subject::Method::Singleton)
private
# Return base path
#
# @return [String]
#
# @api private
#
def base_path
"#{super}/class_methods"
end
end
end
end
end
end
end
end

View file

@ -1,163 +0,0 @@
module Mutant
class Strategy
class Rspec
# Example lookup for rspec
class ExampleLookup
include Adamantium::Flat, Equalizer.new(:mutation)
# Perform example lookup
#
# @param [Mutation] mutation
#
# @return [Enumerable<String>]
#
# @api private
#
def self.run(mutation)
new(mutation).spec_files
end
# Return mutation
#
# @return [Mutation]
#
# @api private
#
attr_reader :mutation
# Return spec files
#
# @return [Enumerable<String>]
#
# @api private
#
def spec_files
expression = glob_expression
files = Dir[expression]
if files.empty?
$stderr.puts("Spec file(s): #{expression.inspect} not found for #{mutation.identification}")
end
files
end
memoize :spec_files
private
# Return method matcher
#
# @return [Matcher]
#
# @api private
#
def matcher
mutation.subject.matcher
end
EXPANSIONS = {
/\?\z/ => '_predicate',
/=\z/ => '_writer',
/!\z/ => '_bang'
}
# Return spec file
#
# @return [String]
#
# @api private
#
def spec_file
"#{mapped_name || expanded_name}_spec.rb"
end
memoize :spec_file
# Return mapped name
#
# @return [Symbol]
# if name was mapped
#
# @return [nil]
# otherwise
#
# @api private
#
def mapped_name
OPERATOR_EXPANSIONS[method_name]
end
# Return expanded name
#
# @return [Symbol]
#
# @api private
#
def expanded_name
EXPANSIONS.inject(method_name) do |name, (regexp, expansion)|
name.to_s.gsub(regexp, expansion)
end.to_sym
end
# Return method name
#
# @return [Symbol]
#
# @api private
#
def method_name
matcher.method_name
end
# Return glob expression
#
# @return [String]
#
# @api private
#
def glob_expression
base = base_path
if mutation.subject.matcher.public?
"#{base}/#{spec_file}"
else
"#{base}/*_spec.rb"
end
end
# Return instance of singleton path appendo
#
# @return [String]
#
# @api private
#
def scope_append
matcher.kind_of?(Matcher::Method::Singleton) ? '/class_methods' : ''
end
memoize :scope_append
# Return base path
#
# @return [String]
#
# @api private
#
def base_path
"spec/unit/#{Inflecto.underscore(mutation.subject.context.scope.name)}#{scope_append}"
end
memoize :base_path
# Initalize object
#
# @param [Mutation] mutation
#
# @api private
#
def initialize(mutation)
@mutation = mutation
end
end
end
end
end

View file

@ -0,0 +1,18 @@
module Mutant
class Strategy
# Static strategies
class Static < self
# Always fail to kill strategy
class Fail < self
KILLER = Killer::Static::Fail
end
# Always succeed to kill strategy
class Success < self
KILLER = Killer::Static::Success
end
end
end
end

View file

@ -1,7 +1,7 @@
module Mutant
# Subject of a mutation
class Subject
include Adamantium::Flat, Enumerable, Equalizer.new(:context, :matcher, :node)
include AbstractType, Adamantium::Flat, Enumerable, Equalizer.new(:context, :node)
# Return context
#
@ -11,14 +11,6 @@ module Mutant
#
attr_reader :context
# Return matcher
#
# @return [Matcher]
#
# @api private
#
attr_reader :matcher
# Return AST node
#
# @return [Rubinius::AST::Node]
@ -40,7 +32,7 @@ module Mutant
def each
return to_enum unless block_given?
Mutator.each(node) do |mutant|
yield Mutation.new(self, mutant)
yield Mutation::Evil.new(self, mutant)
end
self
@ -53,7 +45,7 @@ module Mutant
# @api private
#
def noop
Mutation::Noop.new(self)
Mutation::Neutral.new(self, node)
end
memoize :noop
@ -84,7 +76,7 @@ module Mutant
# @api private
#
def identification
"#{matcher.identification}:#{source_path}:#{source_line}"
"#{subtype}:#{source_path}:#{source_line}"
end
memoize :identification
@ -126,19 +118,27 @@ module Mutant
# Initialize subject
#
# @param [Matcher] matcher
# the context of mutations
# @param [Mutant::Context] context
#
# @param [Rubinius::AST::Node] node
# the node to be mutated
# the original node to be mutated
#
# @return [unkown]
#
# @api private
#
def initialize(matcher, context, node)
@matcher, @context, @node = matcher, context, node
def initialize(context, node)
@context, @node = context, node
end
# Return subtype identifier
#
# @return [String]
#
# @api private
#
abstract_method :subtype
private :subtype
end
end

View file

@ -0,0 +1,104 @@
module Mutant
class Subject
# Abstract base class for method subjects
class Method < self
# Test if method is public
#
# @return [true]
# if method is public
#
# @return [false]
# otherwise
#
# @api private
#
abstract_method :public?
# Return method name
#
# @return [Symbol]
#
# @api private
#
def name
node.name
end
private
# Return scope
#
# @return [Class, Module]
#
# @api private
#
def scope
context.scope
end
# Instance method subjects
class Instance < self
# Test if method is public
#
# @return [true]
# if method is public
#
# @return [false]
# otherwise
#
# @api private
#
def public?
scope.public_method_defined?(name)
end
memoize :public?
private
# Return subtype identifier
#
# @return [String]
#
# @api private
#
def subtype
"#{context.identification}##{name}"
end
end
# Singleton method subjects
class Singleton < self
# Test if method is public
#
# @return [true]
# if method is public
#
# @return [false]
# otherwise
#
# @api private
#
def public?
scope.singleton_class.public_method_defined?(name)
end
memoize :public?
private
# Return subtype identifier
#
# @return [String]
#
# @api private
#
def subtype
"#{context.identification}.#{node.body.name}"
end
end
end
end
end

View file

@ -1,6 +1,7 @@
module Mutant
# A mixing to create method object semantics
module MethodObject
# Hook called when descendant is extended
#
# @param [Module|Class] descendant
@ -27,5 +28,6 @@ module Mutant
def run(*args)
new(*args)
end
end
end

View file

@ -2,7 +2,7 @@
Gem::Specification.new do |gem|
gem.name = 'mutant'
gem.version = '0.2.18'
gem.version = '0.3.0'
gem.authors = [ 'Markus Schirp' ]
gem.email = [ 'mbj@seonic.net' ]
gem.description = 'Mutation testing for ruby'

View file

@ -7,6 +7,48 @@ describe Mutant, 'method matching' do
end
end
this_example = 'Mutant method matching'
shared_examples_for this_example do
subject { Mutant::CLI::Classifier.build(pattern).to_a }
let(:values) { defaults.merge(expectation) }
let(:method_name) { values.fetch(:method_name) }
let(:method_line) { values.fetch(:method_line) }
let(:method_arity) { values.fetch(:method_arity) }
let(:scope) { values.fetch(:scope) }
let(:node_class) { values.fetch(:node_class) }
let(:node) { mutation_subject.node }
let(:context) { mutation_subject.context }
let(:mutation_subject) { subject.first }
it 'should return one subject' do
subject.size.should be(1)
end
it 'should have correct method name' do
name(node).should eql(method_name)
end
it 'should have correct line number' do
node.line.should eql(method_line)
end
it 'should have correct arity' do
arguments(node).required.length.should eql(method_arity)
end
it 'should have correct scope in context' do
context.send(:scope).should eql(scope)
end
it 'should have the correct node class' do
node.should be_a(node_class)
end
end
before do
#eval(body, TOPLEVEL_BINDING, __FILE__, 0)
eval(body)
@ -47,7 +89,7 @@ describe Mutant, 'method matching' do
{ :method_line => 2 }
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
context 'when method is defined multiple times' do
@ -68,7 +110,7 @@ describe Mutant, 'method matching' do
}
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
context 'on the same line' do
@ -87,7 +129,7 @@ describe Mutant, 'method matching' do
}
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
context 'on the same line with differend scope' do
@ -106,7 +148,7 @@ describe Mutant, 'method matching' do
}
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
context 'when nested' do
@ -131,7 +173,7 @@ describe Mutant, 'method matching' do
}
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
context 'in module' do
@ -153,7 +195,7 @@ describe Mutant, 'method matching' do
}
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
end
end
@ -194,7 +236,7 @@ describe Mutant, 'method matching' do
}
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
context 'when defined on constant' do
@ -216,7 +258,7 @@ describe Mutant, 'method matching' do
}
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
context 'outside namespace' do
@ -235,7 +277,7 @@ describe Mutant, 'method matching' do
}
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
end
@ -262,7 +304,7 @@ describe Mutant, 'method matching' do
}
end
it_should_behave_like 'a method match'
it_should_behave_like this_example
end
end
end

View file

@ -11,14 +11,14 @@ describe Mutant,'rspec integration' do
let(:strategy) { Mutant::Strategy::Rspec::DM2 }
specify 'allows to kill mutations' do
Kernel.system("bundle exec mutant -I lib -r test_app --rspec-dm2 ::TestApp::Literal#string").should be(true)
Kernel.system('bundle exec mutant --rspec-dm2 ::TestApp::Literal#string').should be(true)
end
specify 'fails to kill mutations when they are not covered' do
Kernel.system("bundle exec mutant -I lib -r test_app --rspec-dm2 ::TestApp::Literal#uncovered_string").should be(false)
Kernel.system('bundle exec mutant --rspec-dm2 ::TestApp::Literal#uncovered_string').should be(false)
end
specify 'fails when some mutations when are not covered' do
Kernel.system("bundle exec mutant -I lib -r test_app --rspec-dm2 ::TestApp::Literal").should be(false)
Kernel.system('bundle exec mutant --rspec-dm2 ::TestApp::Literal').should be(false)
end
end

View file

@ -1,39 +0,0 @@
shared_examples_for 'a method match' do
subject { Mutant::Matcher::Method.parse(pattern).to_a }
let(:values) { defaults.merge(expectation) }
let(:method_name) { values.fetch(:method_name) }
let(:method_line) { values.fetch(:method_line) }
let(:method_arity) { values.fetch(:method_arity) }
let(:scope) { values.fetch(:scope) }
let(:node_class) { values.fetch(:node_class) }
let(:node) { mutation_subject.node }
let(:context) { mutation_subject.context }
let(:mutation_subject) { subject.first }
it 'should return one subject' do
subject.size.should be(1)
end
it 'should have correct method name' do
name(node).should eql(method_name)
end
it 'should have correct line number' do
node.line.should eql(method_line)
end
it 'should have correct arity' do
arguments(node).required.length.should eql(method_arity)
end
it 'should have correct scope in context' do
context.send(:scope).should eql(scope)
end
it 'should have the correct node class' do
node.should be_a(node_class)
end
end

View file

@ -0,0 +1,9 @@
require 'ice_nine'
module IceNine
class Freezer
class RSpec < NoFreeze
end
end
end

View file

@ -2,32 +2,41 @@ require 'spec_helper'
shared_examples_for 'an invalid cli run' do
it 'should raise error' do
expect { subject }.to raise_error(described_class::Error, expected_message)
expect { subject }.to raise_error(Mutant::CLIParser::Error, expected_message)
end
end
shared_examples_for 'a cli parser' do
its(:filter) { should eql(expected_filter) }
its(:killer) { should eql(expected_killer) }
its(:reporter) { should eql(expected_reporter) }
its(:matcher) { should eql(expected_matcher) }
its(:filter) { should eql(expected_filter) }
its(:strategy) { should eql(expected_strategy.new(subject)) }
its(:reporter) { should eql(expected_reporter) }
its(:matcher) { should eql(expected_matcher) }
end
describe Mutant::CLI, '.new' do
let(:object) { described_class }
let(:time) { Time.now }
before do
pending
Time.stub(:now => time)
end
let(:object) { described_class }
# Defaults
let(:expected_filter) { Mutant::Mutation::Filter::ALL }
let(:expected_killer) { Mutant::Killer::Rspec::Forking }
let(:expected_reporter) { Mutant::Reporter::CLI.new($stderr) }
let(:expected_filter) { Mutant::Mutation::Filter::ALL }
let(:expected_strategy) { Mutant::Strategy::Rspec::Unit }
let(:expected_reporter) { Mutant::Reporter::CLI.new($stderr) }
subject { object.new(arguments) }
context 'with unknown flag' do
let(:arguments) { %w(--invalid) }
let(:expected_message) { 'Unknown option: "--invalid"' }
it_should_behave_like 'an invalid cli run'
end
context 'with unknown option' do
let(:arguments) { %w(--invalid Foo) }
@ -36,41 +45,44 @@ describe Mutant::CLI, '.new' do
it_should_behave_like 'an invalid cli run'
end
context 'with many strategy flags' do
let(:arguments) { %w(--rspec-unit --rspec-dm2) }
let(:expected_strategy) { Mutant::Strategy::Rspec::DM2 }
end
context 'without arguments' do
let(:arguments) { [] }
let(:expected_message) { 'No matchers given' }
let(:expected_message) { 'No strategy was set!' }
it_should_behave_like 'an invalid cli run'
end
context 'with code filter and missing argument' do
let(:arguments) { %w(--rspec-unit --code) }
let(:arguments) { %w(--rspec-unit --code) }
let(:expected_message) { '"--code" is missing an argument' }
it_should_behave_like 'an invalid cli run'
end
context 'with explicit method matcher' do
let(:arguments) { %w(TestApp::Literal#float) }
let(:expected_matcher) { Mutant::Matcher::Method.parse('TestApp::Literal#float') }
let(:arguments) { %w(--rspec-unit TestApp::Literal#float) }
let(:expected_matcher) { Mutant::CLI::Classifier::Method.new('TestApp::Literal#float') }
it_should_behave_like 'a cli parser'
end
context 'with library name' do
let(:arguments) { %w(::TestApp) }
let(:expected_matcher) { Mutant::Matcher::ObjectSpace.new(%r(\ATestApp(\z|::))) }
context 'with namespace matcher' do
let(:arguments) { %w(--rspec-unit ::TestApp*) }
let(:expected_matcher) { Mutant::CLI::Classifier::Namespace.new('::TestApp*') }
it_should_behave_like 'a cli parser'
end
context 'with code filter' do
let(:arguments) { %w(--code faa --code bbb TestApp::Literal#float) }
let(:arguments) { %w(--rspec-unit --code faa --code bbb TestApp::Literal#float) }
let(:filters) do
[
@ -79,8 +91,8 @@ describe Mutant::CLI, '.new' do
]
end
let(:expected_matcher) { Mutant::Matcher::Method.parse('TestApp::Literal#float') }
let(:expected_filter) { Mutant::Mutation::Filter::Whitelist.new(filters) }
let(:expected_matcher) { Mutant::CLI::Classifier::Method.new('TestApp::Literal#float') }
let(:expected_filter) { Mutant::Mutation::Filter::Whitelist.new(filters) }
it_should_behave_like 'a cli parser'
end

View file

@ -6,33 +6,34 @@ describe Mutant::CLI, '.run' do
let(:object) { described_class }
let(:argv) { mock('ARGV') }
let(:attributes) { mock('Options') }
let(:runner) { mock('Runner', :fail? => failure) }
let(:runner) { mock('Runner', :success? => success) }
let(:instance) { mock(described_class.name, :attributes => attributes) }
before do
described_class.stub(:new => instance)
Mutant::Runner.stub(:run => runner)
Mutant::Runner::Config.stub(:run => runner)
end
context 'when runner does NOT fail' do
let(:failure) { false }
context 'when runner is successful' do
let(:success) { true }
it { should be(0) }
it 'should run with attributes' do
Mutant::Runner.should_receive(:run).with(instance).and_return(runner)
Mutant::Runner::Config.should_receive(:run).with(instance).and_return(runner)
should be(0)
end
end
context 'when runner fails' do
let(:failure) { true }
let(:success) { false }
it { should be(1) }
it 'should run with attributes' do
Mutant::Runner.should_receive(:run).with(instance).and_return(runner)
Mutant::Runner::Config.should_receive(:run).with(instance).and_return(runner)
should be(1)
end
end
end

View file

@ -0,0 +1,44 @@
require 'spec_helper'
describe Mutant::CLI::Classifier, '.build' do
subject { described_class.build(input) }
this_spec = 'Mutant::CLI::Classifier.build'
shared_examples_for this_spec do
it 'shoud return expected instance' do
should eql(expected_class.new(expected_class::REGEXP.match(input)))
end
end
context 'with explicit toplevel scope' do
let(:input) { '::TestApp::Literal#string' }
let(:expected_class) { Mutant::CLI::Classifier::Method }
it_should_behave_like this_spec
end
context 'with instance method notation' do
let(:input) { 'TestApp::Literal#string' }
let(:expected_class) { Mutant::CLI::Classifier::Method }
it_should_behave_like this_spec
end
context 'with singleton method notation' do
let(:input) { 'TestApp::Literal.string' }
let(:expected_class) { Mutant::CLI::Classifier::Method }
it_should_behave_like this_spec
end
context 'with invalid notation' do
let(:input) { '::' }
it 'should return nil' do
should be(nil)
end
end
end

View file

@ -1,39 +0,0 @@
require 'spec_helper'
describe Mutant::Killer,'#fail?' do
subject { object.fail? }
let(:object) { class_under_test.new(strategy, mutation) }
let(:strategy) { mock('Strategy') }
let(:mutation) { mock('Mutation') }
before do
mutation.stub(:insert)
mutation.stub(:reset)
end
let(:class_under_test) do
kill_state = self.kill_state
Class.new(described_class) do
define_method :run do
kill_state
end
end
end
context 'when mutant was killed' do
let(:kill_state) { true }
it_should_behave_like 'an idempotent method'
it { should be(false) }
end
context 'when mutant was NOT killed' do
let(:kill_state) { false }
it_should_behave_like 'an idempotent method'
it { should be(true) }
end
end

View file

@ -4,9 +4,10 @@ describe Mutant::Killer::Rspec, '.new' do
subject { object.new(strategy, mutation) }
let(:strategy) { mock('Strategy', :spec_files => ['foo'], :error_stream => $stderr, :output_stream => $stdout) }
let(:context) { mock('Context') }
let(:mutation) { mock('Mutation') }
let(:strategy) { mock('Strategy', :spec_files => ['foo'], :error_stream => $stderr, :output_stream => $stdout) }
let(:context) { mock('Context') }
let(:mutation) { mock('Mutation', :subject => mutation_subject) }
let(:mutation_subject) { mock('Mutation Subject') }
let(:object) { described_class }
@ -19,14 +20,14 @@ describe Mutant::Killer::Rspec, '.new' do
context 'when run exits zero' do
let(:exit_status) { 0 }
its(:fail?) { should be(true) }
its(:killed?) { should be(false) }
it { should be_a(described_class) }
end
context 'when run exits nonzero' do
let(:exit_status) { 1 }
its(:fail?) { should be(false) }
its(:killed?) { should be(true) }
it { should be_a(described_class) }
end
end

View file

@ -0,0 +1,28 @@
require 'spec_helper'
describe Mutant::Killer, '#success?' do
subject { object.success? }
let(:object) { class_under_test.new(strategy, mutation) }
let(:strategy) { mock('Strategy') }
let(:mutation) { mock('Mutation', :success? => kill_state) }
let(:kill_state) { mock('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

View file

@ -1,49 +0,0 @@
require 'spec_helper'
describe Mutant::Matcher, '.from_string' do
subject { object.from_string(input) }
let(:input) { mock('Input') }
let(:matcher) { mock('Matcher') }
let(:descendant_a) { mock('Descendant A', :parse => nil) }
let(:descendant_b) { mock('Descendant B', :parse => nil) }
let(:object) { described_class }
before do
described_class.stub(:descendants => [descendant_a, descendant_b])
end
context 'when no descendant takes the input' do
it { should be(nil) }
it_should_behave_like 'an idempotent method'
end
context 'when one descendant handles input' do
before do
descendant_a.stub(:parse => matcher)
end
it { should be(matcher) }
it_should_behave_like 'an idempotent method'
end
context 'when more than one descendant handles input' do
let(:matcher_b) { mock('Matcher B') }
before do
descendant_a.stub(:parse => matcher)
descendant_b.stub(:parse => matcher_b)
end
it 'should return the first matcher' do
should be(matcher)
end
it_should_behave_like 'an idempotent method'
end
end

View file

@ -1,12 +0,0 @@
require 'spec_helper'
describe Mutant::Matcher, '.parse' do
subject { object.parse(input) }
let(:input) { mock('Input') }
let(:object) { described_class }
it { should be(nil) }
it_should_behave_like 'an idempotent method'
end

View file

@ -1,21 +0,0 @@
require 'spec_helper'
describe Mutant::Matcher::Method, '.parse' do
subject { described_class.parse(input) }
let(:response) { mock('Response') }
let(:input) { mock('Input') }
let(:classifier) { described_class::Classifier }
before do
classifier.stub(:run => response)
end
it { should be(response) }
it 'should call classifier' do
classifier.should_receive(:run).with(input).and_return(response)
subject
end
end

View file

@ -1,52 +0,0 @@
require 'spec_helper'
describe Mutant::Matcher::Method::Classifier, '.run' do
subject { described_class.run(input) }
shared_examples_for 'Mutant::Matcher::Method::Classifier.run' do
before do
expected_class.stub(:new => response)
end
let(:response) { :Response }
it { should be(response) }
it 'should initialize method filter with correct arguments' do
expected_class.should_receive(:new).with(TestApp::Literal, expected_method).and_return(response)
subject
end
end
context 'with explicit toplevel scope' do
let(:input) { '::TestApp::Literal#string' }
let(:expected_class) { Mutant::Matcher::Method::Instance }
let(:expected_method) { TestApp::Literal.instance_method(:string) }
it_should_behave_like 'Mutant::Matcher::Method::Classifier.run'
end
context 'with instance method notation' do
let(:input) { 'TestApp::Literal#string' }
let(:expected_method) { TestApp::Literal.instance_method(:string) }
let(:expected_class) { Mutant::Matcher::Method::Instance }
it_should_behave_like 'Mutant::Matcher::Method::Classifier.run'
end
context 'with singleton method notation' do
let(:input) { 'TestApp::Literal.string' }
let(:expected_method) { TestApp::Literal.method(:string) }
let(:expected_class) { Mutant::Matcher::Method::Singleton }
it_should_behave_like 'Mutant::Matcher::Method::Classifier.run'
end
context 'with invalid notation' do
let(:input) { 'Foo' }
it 'should return nil' do
should be(nil)
end
end
end

View file

@ -0,0 +1,25 @@
require 'spec_helper'
describe Mutant::Matcher::Namespace, '#each' do
subject { object.each { |item| yields << item } }
let(:yields) { [] }
let(:object) { described_class.new(TestApp::Literal) }
let(:singleton_a) { mock('SingletonA', :name => 'TestApp::Literal') }
let(:singleton_b) { mock('SingletonB', :name => 'TestApp::Foo') }
let(:subject_a) { mock('SubjectA') }
let(:subject_b) { mock('SubjectB') }
before do
Mutant::Matcher::Methods::Singleton.stub(:each).with(singleton_a).and_yield(subject_a)
Mutant::Matcher::Methods::Instance.stub(:each).with(singleton_a).and_yield(subject_b)
ObjectSpace.stub(:each_object => [singleton_a, singleton_b])
end
it_should_behave_like 'an #each method'
it 'should yield subjects' do
expect { subject }.to change { yields }.from([]).to([subject_a, subject_b])
end
end

View file

@ -1,24 +0,0 @@
require 'spec_helper'
describe Mutant::Matcher::ObjectSpace, '.parse' do
subject { object.parse(input) }
let(:object) { described_class }
let(:matcher) { mock('Matcher') }
context 'with valid notation' do
let(:input) { '::TestApp::Literal' }
it 'should return matcher' do
described_class.should_receive(:new).with(%r(\ATestApp::Literal(\z|::))).and_return(matcher)
should be(matcher)
end
end
context 'with invalid notation' do
let(:input) { 'TestApp' }
it { should be(nil) }
end
end

View file

@ -1,31 +0,0 @@
require 'spec_helper'
describe Mutant::Matcher::ObjectSpace, '#each' do
before do
pending "defunct"
end
subject { object.each { |item| yields << item } }
let(:yields) { [] }
let(:object) { described_class.new(/\ATestApp::Literal(\z|::)/) }
before do
Mutant::Matcher::Method::Singleton.stub(:each).and_yield(matcher_a)
Mutant::Matcher::Method::Instance.stub(:each).and_yield(matcher_b)
matcher_a.stub(:each).and_yield(subject_a)
matcher_b.stub(:each).and_yield(subject_b)
end
let(:matcher_a) { mock('Matcher A') }
let(:matcher_b) { mock('Matcher B') }
let(:subject_a) { mock('Subject A') }
let(:subject_b) { mock('Subject B') }
it_should_behave_like 'an #each method'
it 'should yield subjects' do
expect { subject }.to change { yields }.from([]).to([subject_a, subject_b])
end
end

View file

@ -0,0 +1,24 @@
require 'spec_helper'
describe Mutant::Runner::Config, '#subjects' do
let(:object) { described_class.run(config) }
subject { object.subjects }
let(:config) { mock('Config', :subjects => [mutation_subject]) }
let(:mutation_subject) { mock('Mutation subject') }
let(:subject_runner) { mock('Subject runner') }
class DummySubjectRunner
include Composition.new(:config, :mutation)
def self.run(*args); new(*args); end
end
before do
stub_const('Mutant::Runner::Subject', DummySubjectRunner)
end
it { should eql([DummySubjectRunner.new(config, mutation_subject)]) }
it_should_behave_like 'an idempotent method'
end

View file

@ -0,0 +1,31 @@
require 'spec_helper'
describe Mutant::Runner::Mutation, '#killer' do
let(:object) { described_class.run(config, mutation) }
let(:config) { mock('Config') }
let(:mutation) { mock('Mutation') }
let(:strategy) { mock('Strategy') }
let(:killer) { mock('Killer') }
subject { object.killer }
before do
config.stub(:strategy => strategy)
strategy.stub(:kill => killer)
end
it 'should call configuration to identify strategy' do
config.should_receive(:strategy).with(mutation).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

View file

@ -0,0 +1,24 @@
require 'spec_helper'
describe Mutant::Runner::Subject, '#mutations' do
let(:object) { described_class.run(config, mutation_subject) }
subject { object.mutations }
let(:config) { mock('Config') }
let(:mutation) { mock('Mutation') }
let(:mutation_subject) { [mutation] }
class DummyRunner
include Composition.new(:config, :mutation)
def self.run(*args); new(*args); end
end
before do
stub_const('Mutant::Runner::Mutation', DummyRunner)
end
it { should eql([DummyRunner.new(config, mutation)]) }
it_should_behave_like 'an idempotent method'
end

View file

@ -0,0 +1,49 @@
require 'spec_helper'
describe Mutant::Strategy::MethodExpansion, '.run' do
subject { object.run(name) }
let(:object) { described_class }
context 'unexpandable and unmapped name' do
let(:name) { :foo }
it { should be(:foo) }
end
context 'expanded name' do
context 'predicate' do
let(:name) { :foo? }
it { should be(:foo_predicate) }
end
context 'writer' do
let(:name) { :foo= }
it { should be(:foo_writer) }
end
context 'bang' do
let(:name) { :foo! }
it { should be(:foo_bang) }
end
end
context 'operator expansions' do
Mutant::OPERATOR_EXPANSIONS.each do |name, expansion|
context "#{name}" do
let(:name) { name }
it "should expand to #{expansion}" do
should be(expansion)
end
end
end
end
end

View file

@ -0,0 +1,52 @@
require 'spec_helper'
describe Mutant::Strategy::Rspec::DM2::Lookup::Method::Instance, '#spec_files' do
subject { object.spec_files }
let(:object) { described_class.new(mutation_subject) }
let(:mutation_subject) { mock('Subject', :name => method_name, :public? => is_public, :context => context) }
let(:context) { mock('Context', :name => 'Foo') }
let(:method_name) { :bar }
let(:files) { 'Files'.freeze }
this_example_group = 'Mutant::Strategy::Rspec::DM2::Lookup::Method::Instance#spec_files'
shared_examples_for this_example_group do
it_should_behave_like 'an idempotent method'
before do
if is_public
Mutant::Strategy::MethodExpansion.should_receive(:run).with(method_name).and_return(:expanded_name)
end
Dir.should_receive(:glob).with(expected_glob_expression).and_return(files)
end
it { should be(files) }
it { should be_frozen }
end
context 'with public method' do
let(:is_public) { true }
let(:expected_glob_expression) { 'spec/unit/foo/expanded_name_spec.rb' }
it_should_behave_like this_example_group
end
context 'with nonpublic method' do
let(:is_public) { false }
context 'non initialize' do
let(:expected_glob_expression) { 'spec/unit/foo/*_spec.rb' }
it_should_behave_like this_example_group
end
context 'initialize' do
let(:method_name) { :initialize }
let(:expected_glob_expression) { 'spec/unit/foo/*_spec.rb spec/unit/foo/class_methods/new_spec.rb' }
it_should_behave_like this_example_group
end
end
end

View file

@ -0,0 +1,42 @@
require 'spec_helper'
describe Mutant::Strategy::Rspec::DM2::Lookup::Method::Singleton, '#spec_files' do
subject { object.spec_files }
let(:object) { described_class.new(mutation_subject) }
let(:mutation_subject) { mock('Subject', :name => method_name, :public? => is_public, :context => context) }
let(:method_name) { :bar }
let(:files) { 'Files'.freeze }
let(:context) { mock('Context', :name => 'Foo') }
this_example_group = 'Mutant::Strategy::Rspec::DM2::Lookup::Method::Singleton#spec_files'
shared_examples_for this_example_group do
it_should_behave_like 'an idempotent method'
before do
if is_public
Mutant::Strategy::MethodExpansion.should_receive(:run).with(method_name).and_return(:expanded_name)
end
Dir.should_receive(:glob).with(expected_glob_expression).and_return(files)
end
it { should be(files) }
it { should be_frozen }
end
context 'with public method' do
let(:is_public) { true }
let(:expected_glob_expression) { 'spec/unit/foo/class_methods/expanded_name_spec.rb' }
it_should_behave_like this_example_group
end
context 'with nonpublic method' do
let(:is_public) { false }
let(:expected_glob_expression) { 'spec/unit/foo/class_methods/*_spec.rb' }
it_should_behave_like this_example_group
end
end

View file

@ -1,236 +0,0 @@
require 'spec_helper'
describe Mutant::Strategy::Rspec::ExampleLookup, '#spec_file' do
let(:object) { described_class.new(mutation) }
let(:mutation) { mock('Mutation', :subject => mutation_subject) }
let(:mutation_subject) { mock('Subject', :matcher => matcher) }
let(:matcher) { mock('Matcher', :method_name => method_name) }
subject { object.send(:spec_file) }
shared_examples_for 'Mutant::Strategy::Rspec::ExampleLookup#spec_file' do
it_should_behave_like 'an idempotent method'
it { should eql(expected_spec_file) }
it { should be_frozen }
end
context 'negation operator' do
let(:method_name) { :'!' }
let(:expected_spec_file) { 'negation_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with unary match method' do
let(:method_name) { :~@ }
let(:expected_spec_file) { 'unary_match_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with unary substraction method' do
let(:method_name) { :-@ }
let(:expected_spec_file) { 'unary_substraction_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with unary addition method' do
let(:method_name) { :+@ }
let(:expected_spec_file) { 'unary_addition_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with bitwise xor method' do
let(:method_name) { :^ }
let(:expected_spec_file) { 'bitwise_xor_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with bitwise or method' do
let(:method_name) { :| }
let(:expected_spec_file) { 'bitwise_or_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with bitwise and method' do
let(:method_name) { :& }
let(:expected_spec_file) { 'bitwise_and_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with spaceship method' do
let(:method_name) { :<=> }
let(:expected_spec_file) { 'spaceship_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with case equality operator method' do
let(:method_name) { :=== }
let(:expected_spec_file) { 'case_equality_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with modulo operator method' do
let(:method_name) { :% }
let(:expected_spec_file) { 'modulo_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with exponentation operator method' do
let(:method_name) { :** }
let(:expected_spec_file) { 'exponentation_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with substraction operator method' do
let(:method_name) { :- }
let(:expected_spec_file) { 'substraction_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with addition operator method' do
let(:method_name) { :+ }
let(:expected_spec_file) { 'addition_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with greater than or equal to operator method' do
let(:method_name) { :>= }
let(:expected_spec_file) { 'greater_than_or_equal_to_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with less than or equal to operator method' do
let(:method_name) { :<= }
let(:expected_spec_file) { 'less_than_or_equal_to_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with greater than operator method' do
let(:method_name) { :> }
let(:expected_spec_file) { 'greater_than_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with less than operator method' do
let(:method_name) { :< }
let(:expected_spec_file) { 'less_than_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with right shift operator method' do
let(:method_name) { :>> }
let(:expected_spec_file) { 'right_shift_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with left shift operator method' do
let(:method_name) { :<< }
let(:expected_spec_file) { 'left_shift_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with division operator method' do
let(:method_name) { :/ }
let(:expected_spec_file) { 'division_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with multiplication operator method' do
let(:method_name) { :* }
let(:expected_spec_file) { 'multiplication_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with nomatch operator method' do
let(:method_name) { :'!~' }
let(:expected_spec_file) { 'nomatch_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with match operator method' do
let(:method_name) { :=~ }
let(:expected_spec_file) { 'match_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with inequality operator method' do
let(:method_name) { :'!=' }
let(:expected_spec_file) { 'inequality_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with equality operator method' do
let(:method_name) { :== }
let(:expected_spec_file) { 'equality_operator_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with element reader method' do
let(:method_name) { :[] }
let(:expected_spec_file) { 'element_reader_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with element writer method' do
let(:method_name) { :[]= }
let(:expected_spec_file) { 'element_writer_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with writer method' do
let(:method_name) { :foo= }
let(:expected_spec_file) { 'foo_writer_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with bang method' do
let(:method_name) { :foo! }
let(:expected_spec_file) { 'foo_bang_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with predicate method' do
let(:method_name) { :foo? }
let(:expected_spec_file) { 'foo_predicate_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
context 'with regular method' do
let(:method_name) { :foo }
let(:expected_spec_file) { 'foo_spec.rb' }
it_should_behave_like 'Mutant::Strategy::Rspec::ExampleLookup#spec_file'
end
end

View file

@ -1,13 +0,0 @@
require 'spec_helper'
describe Mutant::Subject, '.new' do
subject { object.new(matcher, context, ast) }
let(:object) { described_class }
let(:matcher) { mock('Matcher') }
let(:context) { mock('Context') }
let(:ast) { mock('AST') }
it { should be_frozen }
end

View file

@ -3,9 +3,12 @@ require 'spec_helper'
describe Mutant::Subject, '#context' do
subject { object.context }
let(:object) { described_class.new(matcher, context, ast) }
let(:matcher) { mock('Matcher') }
let(:ast) { mock('AST') }
let(:class_under_test) do
Class.new(described_class)
end
let(:object) { class_under_test.new(context, node) }
let(:node) { mock('Node') }
let(:context) { mock('Context') }
it { should be(context) }

View file

@ -3,14 +3,17 @@ require 'spec_helper'
describe Mutant::Subject, '#each' do
subject { object.each { |item| yields << item } }
let(:object) { described_class.new(matcher, context, ast) }
let(:matcher) { mock('Matcher') }
let(:root) { mock('Root AST') }
let(:ast) { mock('AST') }
let(:context) { mock('Context', :root => root) }
let(:mutant) { mock('Mutant') }
let(:mutation) { mock('Mutation') }
let(:yields) { [] }
let(:class_under_test) do
Class.new(described_class)
end
let(:object) { class_under_test.new(context, ast) }
let(:root) { mock('Root Node') }
let(:ast) { mock('Node') }
let(:context) { mock('Context', :root => root) }
let(:mutant) { mock('Mutant') }
let(:mutation) { mock('Mutation') }
let(:yields) { [] }
before do
Mutant::Mutator.stub(:each).with(ast).and_yield(mutant).and_return(Mutant::Mutator)

View file

@ -2,8 +2,12 @@ require 'spec_helper'
describe Mutant::Subject, '#node' do
subject { object.node }
let(:object) { described_class.new(matcher, context, node) }
let(:matcher) { mock('Matcher') }
let(:class_under_test) do
Class.new(described_class)
end
let(:object) { class_under_test.new(context, node) }
let(:node) { mock('Node') }
let(:context) { mock('Context') }

View file

@ -1,7 +0,0 @@
desc 'Run metrics with Heckle'
task :ci => %w[ ci:metrics heckle ]
namespace :ci do
desc 'Run metrics'
task :metrics => %w[ verify_measurements flog flay reek roodi metrics:all ]
end

View file

@ -1,41 +0,0 @@
begin
require 'flay'
require 'yaml'
config = YAML.load_file(File.expand_path('../../../config/flay.yml', __FILE__)).freeze
threshold = config.fetch('threshold').to_i
total_score = config.fetch('total_score').to_f
files = Flay.expand_dirs_to_files(config.fetch('path', 'lib'))
# original code by Marty Andrews:
# http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html
desc 'Analyze for code duplication'
task :flay do
# run flay once without a threshold to ensure the max mass matches the threshold
flay = Flay.new(:fuzzy => false, :verbose => false, :mass => 0)
flay.process(*files)
max = flay.masses.map { |hash, mass| mass.to_f / flay.hashes[hash].size }.max
unless max >= threshold
raise "Adjust flay threshold down to #{max}"
end
total = flay.masses.reduce(0.0) { |total, (hash, mass)| total + (mass.to_f / flay.hashes[hash].size) }
unless total == total_score
raise "Flay total is now #{total}, but expected #{total_score}"
end
# run flay a second time with the threshold set
flay = Flay.new(:fuzzy => false, :verbose => false, :mass => threshold.succ)
flay.process(*files)
if flay.masses.any?
flay.report
raise "#{flay.masses.size} chunks of code have a duplicate mass > #{threshold}"
end
end
rescue LoadError
task :flay do
abort 'Flay is not available. In order to run flay, you must: gem install flay'
end
end

View file

@ -1,43 +0,0 @@
begin
require 'flog'
require 'yaml'
class Float
def round_to(n)
(self * 10**n).round.to_f * 10**-n
end
end
config = YAML.load_file(File.expand_path('../../../config/flog.yml', __FILE__)).freeze
threshold = config.fetch('threshold').to_f.round_to(1)
# original code by Marty Andrews:
# http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html
desc 'Analyze for code complexity'
task :flog do
flog = Flog.new
flog.flog Array(config.fetch('path', 'lib'))
totals = flog.totals.select { |name, score| name[-5, 5] != '#none' }.
map { |name, score| [ name, score.round_to(1) ] }.
sort_by { |name, score| score }
max = totals.last[1]
unless max >= threshold
raise "Adjust flog score down to #{max}"
end
bad_methods = totals.select { |name, score| score > threshold }
if bad_methods.any?
bad_methods.reverse_each do |name, score|
puts '%8.1f: %s' % [ score, name ]
end
raise "#{bad_methods.size} methods have a flog complexity > #{threshold}"
end
end
rescue LoadError
task :flog do
abort 'Flog is not available. In order to run flog, you must: gem install flog'
end
end

View file

@ -1,216 +0,0 @@
$LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__))
# original code by Ashley Moran:
# http://aviewfromafar.net/2007/11/1/rake-task-for-heckling-your-specs
begin
require 'pathname'
require 'backports'
require 'active_support/inflector'
require 'heckle'
require 'mspec'
require 'mspec/utils/name_map'
SKIP_METHODS = %w[ blank_slate_method_added ].freeze
class NameMap
def file_name(method, constant)
map = MAP[method]
name = if map
map[constant] || map[:default]
else
method.
gsub('?','_ques').
gsub('!','_bang').
gsub('=','_assign')
end
"#{name}_spec.rb"
end
end
desc 'Heckle each module and class'
task :heckle do
unless Ruby2Ruby::VERSION == '1.2.2'
raise "ruby2ruby version #{Ruby2Ruby::VERSION} may not work properly, 1.2.2 *only* is recommended for use with heckle"
end
require File.expand_path('../../../spec/support/fake_ast',__FILE__)
require 'mutant'
root_module_regexp = Regexp.union('Mutant')
spec_dir = Pathname('spec/unit')
NameMap::MAP.each do |op, method|
next if method.kind_of?(Hash)
NameMap::MAP[op] = { :default => method }
end
aliases = Hash.new { |h,mod| h[mod] = Hash.new { |h,method| h[method] = method } }
map = NameMap.new
heckle_caught_modules = Hash.new { |hash, key| hash[key] = [] }
unhandled_mutations = 0
ObjectSpace.each_object(Module) do |mod|
next unless mod.name =~ /\A#{root_module_regexp}(?::|\z)/
# Mutation::Loader is to rbx specific
next if mod == Mutant::Loader
# Mutation::Matcher::Method is to rbx specific
next if mod == Mutant::Matcher::Method
# Mutation::Context::Constant is to rbx specific
next if mod == Mutant::Context::Constant
spec_prefix = spec_dir.join(mod.name.underscore)
specs = []
# get the public class methods
metaclass = class << mod; self end
ancestors = metaclass.ancestors
spec_class_methods = mod.singleton_methods(false)
spec_class_methods.reject! do |method|
%w[ yaml_new yaml_tag_subclasses? included nesting constants ].include?(method.to_s)
end
if mod.ancestors.include?(Singleton)
spec_class_methods.reject! { |method| method.to_s == 'instance' }
end
# get the protected and private class methods
other_class_methods = metaclass.protected_instance_methods(false) |
metaclass.private_instance_methods(false)
ancestors.each do |ancestor|
other_class_methods -= ancestor.protected_instance_methods(false) |
ancestor.private_instance_methods(false)
end
other_class_methods.reject! do |method|
method.to_s == 'allocate' || SKIP_METHODS.include?(method.to_s)
end
other_class_methods.reject! do |method|
next unless spec_class_methods.any? { |specced| specced.to_s == $1 }
spec_class_methods << method
end
# get the instances methods
spec_methods = mod.public_instance_methods(false)
other_methods = mod.protected_instance_methods(false) |
mod.private_instance_methods(false)
other_methods.reject! do |method|
next unless spec_methods.any? { |specced| specced.to_s == $1 }
spec_methods << method
end
# map the class methods to spec files
spec_class_methods.each do |method|
method = aliases[mod.name][method]
next if SKIP_METHODS.include?(method.to_s)
spec_file = spec_prefix.join('class_methods').join(map.file_name(method, mod.name))
unless spec_file.file?
raise "No spec file #{spec_file} for #{mod}.#{method}"
next
end
specs << [ ".#{method}", [ spec_file ] ]
end
# map the instance methods to spec files
spec_methods.each do |method|
method = aliases[mod.name][method]
next if SKIP_METHODS.include?(method.to_s)
spec_file = spec_prefix.join(map.file_name(method, mod.name))
unless spec_file.file?
raise "No spec file #{spec_file} for #{mod}##{method}"
next
end
specs << [ "##{method}", [ spec_file ] ]
end
# non-public methods are considered covered if they can be mutated
# and any spec fails for the current or descendant modules
other_methods.each do |method|
descedant_specs = []
ObjectSpace.each_object(Module) do |descedant|
next unless descedant.name =~ /\A#{root_module_regexp}(?::|\z)/ && mod >= descedant
descedant_spec_prefix = spec_dir.join(descedant.name.underscore)
descedant_specs << descedant_spec_prefix
if method.to_s == 'initialize'
descedant_specs.concat(Pathname.glob(descedant_spec_prefix.join('class_methods/new_spec.rb')))
end
end
specs << [ "##{method}", descedant_specs ]
end
other_class_methods.each do |method|
descedant_specs = []
ObjectSpace.each_object(Module) do |descedant|
next unless descedant.name =~ /\A#{root_module_regexp}(?::|\z)/ && mod >= descedant
descedant_specs << spec_dir.join(descedant.name.underscore).join('class_methods')
end
specs << [ ".#{method}", descedant_specs ]
end
specs.sort.each do |(method, spec_files)|
puts "Heckling #{mod}#{method}"
IO.popen("spec #{spec_files.join(' ')} --heckle '#{mod}#{method}'") do |pipe|
while line = pipe.gets
case line = line.chomp
when "The following mutations didn't cause test failures:"
heckle_caught_modules[mod.name] << method
when '+++ mutation'
unhandled_mutations += 1
end
end
end
end
end
if unhandled_mutations > 0
error_message_lines = [ "*************\n" ]
error_message_lines << "Heckle found #{unhandled_mutations} " \
"mutation#{"s" unless unhandled_mutations == 1} " \
"that didn't cause spec violations\n"
heckle_caught_modules.each do |mod, methods|
error_message_lines << "#{mod} contains the following " \
'poorly-specified methods:'
methods.each do |method|
error_message_lines << " - #{method}"
end
error_message_lines << ''
end
error_message_lines << 'Get your act together and come back ' \
'when your specs are doing their job!'
raise error_message_lines.join("\n")
else
puts 'Well done! Your code withstood a heckling.'
end
end
rescue LoadError
task :heckle => :spec do
$stderr.puts 'Heckle or mspec is not available. In order to run heckle, you must: gem install heckle mspec'
end
end

View file

@ -1,31 +0,0 @@
begin
# Require veritas before metric foo pulls AS
require 'veritas'
require 'metric_fu'
require 'json'
# XXX: temporary hack until metric_fu is fixed
MetricFu::Saikuro.class_eval { include FileUtils }
MetricFu::Configuration.run do |config|
config.rcov = {
:environment => 'test',
:test_files => %w[ spec/**/*_spec.rb ],
:rcov_opts => %w[
--sort coverage
--no-html
--text-coverage
--no-color
--profile
--exclude spec/,^/
--include lib:spec
],
}
end
rescue LoadError
namespace :metrics do
task :all do
$stderr.puts 'metric_fu is not available. In order to run metrics:all, you must: gem install metric_fu'
end
end
end

View file

@ -1,15 +0,0 @@
begin
require 'reek/rake/task'
if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'rbx'
task :reek do
$stderr.puts 'Reek fails under rubinius, fix rubinius and remove guard'
end
else
Reek::Rake::Task.new
end
rescue LoadError
task :reek do
$stderr.puts 'Reek is not available. In order to run reek, you must: gem install reek'
end
end

View file

@ -1,15 +0,0 @@
begin
require 'roodi'
require 'rake/tasklib'
require 'roodi_task'
RoodiTask.new do |t|
t.verbose = false
t.config = File.expand_path('../../../config/roodi.yml', __FILE__)
t.patterns = %w[ lib/**/*.rb ]
end
rescue LoadError
task :roodi do
abort 'Roodi is not available. In order to run roodi, you must: gem install roodi'
end
end

View file

@ -1,23 +0,0 @@
begin
require 'pathname'
require 'yardstick'
require 'yardstick/rake/measurement'
require 'yardstick/rake/verify'
require 'yaml'
config = YAML.load_file(File.expand_path('../../../config/yardstick.yml', __FILE__))
# yardstick_measure task
Yardstick::Rake::Measurement.new
# verify_measurements task
Yardstick::Rake::Verify.new do |verify|
verify.threshold = config.fetch('threshold')
end
rescue LoadError
%w[ yardstick_measure verify_measurements ].each do |name|
task name.to_s do
abort "Yardstick is not available. In order to run #{name}, you must: gem install yardstick"
end
end
end

View file

@ -1,45 +0,0 @@
begin
begin
require 'rspec/core/rake_task'
rescue LoadError
require 'spec/rake/spectask'
module RSpec
module Core
RakeTask = Spec::Rake::SpecTask
end
end
end
desc 'run all specs'
task :spec => %w[ spec:unit spec:integration ]
namespace :spec do
RSpec::Core::RakeTask.new(:integration) do |t|
t.pattern = 'spec/integration/**/*_spec.rb'
end
RSpec::Core::RakeTask.new(:unit) do |t|
t.pattern = 'spec/unit/**/*_spec.rb'
end
end
rescue LoadError
task :spec do
abort 'rspec is not available. In order to run spec, you must: gem install rspec'
end
end
begin
desc "Generate code coverage"
RSpec::Core::RakeTask.new(:rcov) do |t|
t.rcov = true
t.rcov_opts = File.read('spec/rcov.opts').split(/\s+/)
end
rescue LoadError
task :rcov do
abort 'rcov is not available. In order to run rcov, you must: gem install rcov'
end
end
task :test => 'spec'

View file

@ -1,9 +0,0 @@
begin
require 'yard'
YARD::Rake::YardocTask.new
rescue LoadError
task :yard do
abort 'YARD is not available. In order to run yard, you must: gem install yard'
end
end

View file

@ -1,6 +1,9 @@
# encoding: utf-8
require 'rspec'
$: << File.join(File.dirname(__FILE__), 'lib')
require 'test_app'
# require spec support files and shared behavior