Introduce noop mutation guards and argument mutators

* Sorry for not splitting up into smaller commit
This commit is contained in:
Markus Schirp 2012-12-11 00:17:19 +01:00
parent 5fed2cb57f
commit 40d5230c68
24 changed files with 478 additions and 133 deletions

View file

@ -1,6 +1,10 @@
# v0.2.4 2012-12-08
* [feature] define block arguments
* [feature] Run noop mutation per subject to guard against initial failing specs
* [feature] Mutate default into required arguments
* [feature] Mutate default literals
* [feature] Mutate unwinding of pattern args |(a, b), c] => |a, b, c|
* [feature] Mutate define and block arguments
* [feature] Mutate block arguments, inklusive pattern args
* [feature] Recurse into block bodies
* [fixed] Crash on mutating yield, added a noop for now

View file

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

View file

@ -9,11 +9,21 @@ require 'digest/sha1'
require 'to_source'
require 'inflector'
require 'ice_nine'
require 'ice_nine/core_ext/object'
require 'diff/lcs'
require 'diff/lcs/hunk'
require 'rspec'
module IceNine
class Freezer
class Rubinius
class AST < IceNine::Freezer::Object
class Node < IceNine::Freezer::Object
end
end
end
end
end
# Library namespace
module Mutant
@ -74,6 +84,7 @@ require 'mutant/mutator/node/send'
require 'mutant/mutator/node/arguments'
require 'mutant/mutator/node/define'
require 'mutant/mutator/node/return'
require 'mutant/mutator/node/local_variable_assignment'
require 'mutant/mutator/node/iter_19'
require 'mutant/mutator/node/if_statement'
require 'mutant/mutator/node/receiver_case'

View file

@ -1,7 +1,7 @@
module Mutant
# Comandline parser
class CLI
include Adamantium::Flat, Equalizer.new(:matcher, :filter, :killer)
include Adamantium::Flat, Equalizer.new(:matcher, :filter, :strategy, :reporter)
# Error raised when CLI argv is inalid
Error = Class.new(RuntimeError)
@ -44,6 +44,20 @@ module Mutant
end
memoize :matcher
# Test for running in debug mode
#
# @return [true]
# if debug mode is active
#
# @return [false]
# otherwise
#
# @api private
#
def debug?
!!@debug
end
# Return mutation filter
#
# @return [Mutant::Matcher]
@ -67,7 +81,9 @@ module Mutant
#
def strategy
@strategy || raise(Error, 'no strategy was set!')
@strategy.new(self)
end
memoize :strategy
# Return reporter
#
@ -76,7 +92,7 @@ module Mutant
# @api private
#
def reporter
Mutant::Reporter::CLI.new($stdout)
Mutant::Reporter::CLI.new(self)
end
memoize :reporter
@ -88,7 +104,8 @@ module Mutant
'--include' => [:add_load_path ],
'-r' => [:require_library ],
'--require' => [:require_library ],
#'--killer-fork' => [:enable_killer_fork ],
'--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 ],
@ -98,33 +115,6 @@ module Mutant
OPTION_PATTERN = %r(\A-(?:-)?[a-zA-Z0-9\-]+\z).freeze
# Return selected killer
#
# @return [Killer]
#
# @api private
#
def selected_killer
unless @rspec
raise Error, "Only rspec is supported currently use --rspec switch"
end
Mutant::Killer::Rspec
end
memoize :selected_killer
# Return option for argument with index
#
# @param [Fixnum] index
#
# @return [String]
#
# @api private
#
def option(index)
@arguments.fetch(index+1)
end
# Initialize CLI
#
# @param [Array<String>] arguments
@ -146,6 +136,18 @@ module Mutant
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]
@ -268,17 +270,15 @@ module Mutant
@rspec = true
end
# Enable killer forking
# Set debug mode
#
# @api private
#
# @return [self]
# @return [undefined]
#
# @api private
#
def enable_killer_fork
def set_debug
consume(1)
@forking = true
@debug = true
end
# Set strategy
@ -304,6 +304,5 @@ module Mutant
require(current_option_value)
consume(2)
end
end
end

View file

@ -29,7 +29,7 @@ module Mutant
#
def run
mutation.insert
!::RSpec::Core::Runner.run(command_line_arguments, @error_stream, @output_stream).zero?
!::RSpec::Core::Runner.run(command_line_arguments, strategy.error_stream, strategy.output_stream).zero?
end
memoize :run

View file

@ -109,5 +109,31 @@ module Mutant
def initialize(subject, node)
@subject, @node = subject, node
end
class Noop < self
# Initialihe 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

@ -32,6 +32,16 @@ module Mutant
end
private_class_method :handle
# Return identity of object (for deduplication)
#
# @param [Object]
#
# @return [Object]
#
def self.identity(object)
object
end
# Return input
#
# @return [Object]
@ -52,12 +62,13 @@ module Mutant
# @api private
#
def initialize(input, block)
@input, @block = Helper.deep_clone(input), block
IceNine.deep_freeze(@input)
@input, @block = IceNine.deep_freeze(input), block
@seen = Set.new
guard(input)
dispatch
end
# Test if generated object is different from input
# Test if generated object is not guarded from emmitting
#
# @param [Object] object
#
@ -69,7 +80,19 @@ module Mutant
# @api private
#
def new?(object)
input != object
!@seen.include?(self.class.identity(object))
end
# Add object to guarded values
#
# @param [Object] object
#
# @return [undefined]
#
# @api private
#
def guard(object)
@seen << self.class.identity(object)
end
# Test if generated mutation is allowed
@ -105,6 +128,8 @@ module Mutant
def emit(object)
return unless new?(object) and allow?(object)
guard(object)
emit!(object)
end
@ -160,7 +185,7 @@ module Mutant
# @api private
#
def dup_input
input.dup
Helper.deep_clone(input)
end
end

View file

@ -5,36 +5,21 @@ module Mutant
class Node < self
include AbstractType
# Return identity of node
#
# @param [Rubinius::AST::Node] node
#
# @return [String]
#
def self.identity(node)
ToSource.to_source(node)
end
private
alias_method :node, :input
alias_method :dup_node, :dup_input
# Return source of input node
#
# @return [String]
#
# @api private
#
def source
ToSource.to_source(node)
end
memoize :source
# Test if generated node is new
#
# @return [true]
# if generated node is different from input
#
# @return [false]
# otherwise
#
# @api private
#
def new?(node)
source != ToSource.to_source(node)
end
# Emit a new AST node
#
# @param [Rubinis::AST::Node:Class] node_class
@ -116,8 +101,8 @@ module Mutant
Mutator.each(body) do |mutation|
dup = dup_node
yield mutation if block_given?
dup.public_send(:"#{name}=", mutation)
yield dup if block_given?
emit(dup)
end
end
@ -156,9 +141,7 @@ module Mutant
#
# @api private
#
def dup_node
node.dup
end
alias_method :dup_node, :dup_input
end
end
end

View file

@ -17,23 +17,24 @@ module Mutant
def dispatch
emit_attribute_mutations(:name)
end
end
# Test if node is new
# Mutantor for default arguments
class DefaultArguments < self
handle(Rubinius::AST::DefaultArguments)
private
# Emit mutations
#
# Note: to_source does not handle PatternVariableNodes as entry points
#
# @param [Rubinius::AST::Node] generated
#
# @return [true]
# if node is new
#
# @return [false]
# otherwise
# @return [undefined]
#
# @api private
#
def new?(generated)
node.name != generated.name
def dispatch
emit_attribute_mutations(:arguments) do |argument|
argument.names = argument.arguments.map(&:name)
end
end
end
@ -53,9 +54,7 @@ module Mutant
def dispatch
Mutator.each(node.arguments.body) do |mutation|
dup = dup_node
dup_args = dup.arguments.dup
dup_args.body = mutation
dup.arguments = dup_args
dup.arguments.body = mutation
emit(dup)
end
end
@ -90,7 +89,51 @@ module Mutant
#
def dispatch
expand_pattern_args
emit_attribute_mutations(:required)
emit_default_mutations
emit_required_defaults_mutation
emit_attribute_mutations(:required) do |mutation|
mutation.names = mutation.optional + mutation.required
end
end
# Emit default mutations
#
# @return [undefined]
#
# @api private
#
def emit_default_mutations
return unless node.defaults
emit_attribute_mutations(:defaults) do |mutation|
mutation.optional = mutation.defaults.names
mutation.names = mutation.required + mutation.optional
if mutation.defaults.names.empty?
mutation.defaults = nil
end
end
end
# Emit required defaults mutations
#
# @return [undefined]
#
# @api private
#
def emit_required_defaults_mutation
return unless node.defaults
arguments = node.defaults.arguments
arguments.each_index do |index|
names = arguments.take(index+1).map(&:name)
dup = dup_node
defaults = dup.defaults
defaults.arguments = defaults.arguments.drop(names.size)
names.each { |name| dup.optional.delete(name) }
dup.required.concat(names)
if dup.optional.empty?
dup.defaults = nil
end
emit(dup)
end
end
# Emit pattern args expansions
@ -102,13 +145,12 @@ module Mutant
def expand_pattern_args
node.required.each_with_index do |argument, index|
next unless argument.kind_of?(Rubinius::AST::PatternArguments)
required = node.required.dup
dup = dup_node
required = dup.required
required.delete_at(index)
argument.arguments.body.reverse.each do |node|
required.insert(index, node.name)
end
dup = dup_node
dup.required = required
dup.names |= required
emit(dup)
end

View file

@ -15,7 +15,8 @@ module Mutant
def dispatch
emit_attribute_mutations(:body)
emit_attribute_mutations(:arguments) do |mutation|
mutation.names = mutation.required
arguments = mutation.arguments
arguments.names = arguments.required + arguments.optional
end if node.arguments
end

View file

@ -0,0 +1,25 @@
module Mutant
class Mutator
class Node
class LocalVariableAssignment < self
handle(Rubinius::AST::LocalVariableAssignment)
private
# Emit mutants
#
# @return [undefined]
#
# @api private
#
def dispatch
emit_attribute_mutations(:name)
emit_attribute_mutations(:value)
end
end
end
end
end

View file

@ -34,6 +34,7 @@ module Mutant
handle(Rubinius::AST::File)
handle(Rubinius::AST::DynamicRegex)
handle(Rubinius::AST::OpAssignOr19)
handle(Rubinius::AST::BlockPass19)
handle(Rubinius::AST::OpAssign1)
handle(Rubinius::AST::Or)
handle(Rubinius::AST::ConstantAccess)

View file

@ -52,5 +52,33 @@ module Mutant
# @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
end
end
end

View file

@ -17,6 +17,26 @@ module Mutant
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
@ -26,7 +46,10 @@ module Mutant
# @api private
#
def mutation(mutation)
#colorized_diff(mutation.original_source, mutation.source)
if @config.debug?
colorized_diff(mutation)
end
self
end
@ -45,6 +68,32 @@ module Mutant
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
puts(colorize(color, "#{word}: #{killer.identification} (%02.2fs)" % killer.runtime))
unless killer.fail?
puts(killer.mutation.source)
stats.noop_fail(killer)
end
self
end
# Reporter killer
#
# @param [Killer] killer
@ -66,8 +115,7 @@ module Mutant
puts(colorize(color, "#{word}: #{killer.identification} (%02.2fs)" % killer.runtime))
if killer.fail?
mutation = killer.mutation
colorized_diff(mutation.original_source, mutation.source)
colorized_diff(killer.mutation)
end
self
@ -87,12 +135,13 @@ module Mutant
end
puts
puts "subjects: #{stats.subject}"
puts "mutations: #{stats.mutation}"
puts "kills: #{stats.kill}"
puts "alive: #{stats.alive}"
puts "mtime: %02.2fs" % stats.time
puts "rtime: %02.2fs" % stats.runtime
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
@ -121,8 +170,9 @@ module Mutant
#
# @api private
#
def initialize(io)
@io = io
def initialize(config)
super
@io = $stdout
@stats = Stats.new
end
@ -136,7 +186,7 @@ module Mutant
#
def failure(killer)
puts(colorize(Color::RED, "!!! Mutant alive: #{killer.identification} !!!"))
colorized_diff(killer.original_source, killer.mutation_source)
colorized_diff(killer.mutation)
puts("Took: (%02.2fs)" % killer.runtime)
end
@ -187,17 +237,25 @@ module Mutant
# @param [String] original
# @param [String] current
#
# @return [self]
# @return [undefined]
#
# @api private
#
def colorized_diff(original, current)
def colorized_diff(mutation)
if mutation.kind_of?(Mutation::Noop)
io.mutation.original_source
return
end
original, current = mutation.original_source, mutation.source
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!"
end
puts(diff)
self
end

View file

@ -1,7 +1,8 @@
module Mutant
class Reporter
# Null reporter
Null = Class.new(self) do
class Null < self
# Report subject
#
# @param [Subject] subject
@ -37,6 +38,7 @@ module Mutant
def killer(*)
self
end
end.new.freeze
end
end
end

View file

@ -10,7 +10,7 @@ module Mutant
#
# @api private
#
attr_reader :subject
attr_reader :subjects
# Return mutation count
#
@ -18,7 +18,15 @@ module Mutant
#
# @api private
#
attr_reader :mutation
attr_reader :mutations
# Return skip count
#
# @return [Fixnum]
#
# @api private
#
attr_reader :noop_fails
# Return kill count
#
@ -26,7 +34,7 @@ module Mutant
#
# @api private
#
attr_reader :kill
attr_reader :kills
# Return mutation runtime
#
@ -38,7 +46,7 @@ module Mutant
def initialize
@start = Time.now
@subject = @mutation = @kill = @time = 0
@noop_fails = @subjects = @mutations = @kills = @time = 0
end
def runtime
@ -46,19 +54,27 @@ module Mutant
end
def subject
@subject +=1
@subjects +=1
self
end
def alive
@mutation - @kill
@mutations - @kills
end
def noop_fail(killer)
@noop_fails += 1
@time += killer.runtime
self
end
def killer(killer)
@mutation +=1
@kill +=1 unless killer.fail?
@mutations +=1
@kills +=1 unless killer.fail?
@time += killer.runtime
self
end
end
end
end
end

View file

@ -86,6 +86,7 @@ module Mutant
# @api private
#
def run_subject(subject)
return unless noop(subject)
subject.each do |mutation|
next unless config.filter.match?(mutation)
reporter.mutation(mutation)
@ -93,20 +94,58 @@ module Mutant
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
false
end
true
end
# Run killer on mutation
#
# @param [Mutation] mutation
#
# @return [undefined]
# @return [true]
# if killer was unsuccessful
#
# @return [false]
# otherwise
#
# @api private
#
def kill(mutation)
killer = config.strategy.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

@ -1,6 +1,46 @@
module Mutant
class Strategy
include AbstractType
include AbstractType, Adamantium::Flat, Equalizer.new
# Return config
#
# @return [Config]
#
# @api private
#
attr_reader :config
# Initialize object
#
# @param [Config] config
#
# @return [undefined
#
# @api private
#
def initialize(config)
@config = config
end
# Return output stream
#
# @return [IO]
#
# @api private
#
def output_stream
config.reporter.output_stream
end
# Return error stream
#
# @return [IO]
#
# @api private
#
def error_stream
config.reporter.error_stream
end
# Kill mutation
#
@ -10,7 +50,7 @@ module Mutant
#
# @api private
#
def self.kill(mutation)
def kill(mutation)
killer.new(self, mutation)
end
@ -20,12 +60,13 @@ module Mutant
#
# @api private
#
def self.killer
self::KILLER
def killer
self.class::KILLER
end
# Static strategies
class Static < self
include Equalizer.new
# Always fail to kill strategy
class Fail < self

View file

@ -15,7 +15,7 @@ module Mutant
#
# @api private
#
def self.spec_files(mutation)
def spec_files(mutation)
ExampleLookup.run(mutation)
end
end
@ -29,7 +29,7 @@ module Mutant
#
# @api private
#
def self.spec_files(mutation)
def spec_files(mutation)
['spec/unit']
end
end
@ -43,14 +43,14 @@ module Mutant
#
# @api private
#
def self.spec_files(mutation)
def spec_files(mutation)
Dir['spec/integration/**/*_spec.rb']
end
end
# Run all specs per mutation
class Full < self
def self.spec_files(mutation)
def spec_files(mutation)
Dir['spec/**/*_spec.rb']
end
end

View file

@ -46,6 +46,17 @@ module Mutant
self
end
# Return noop mutation
#
# @return [Mutation::Noop]
#
# @api private
#
def noop
Mutation::Noop.new(self)
end
memoize :noop
# Return subject identicication
#
# @return [String]

View file

@ -15,13 +15,25 @@ shared_examples_for 'a mutator' do
it { should be_instance_of(to_enum.class) }
def assert_transitive(ast)
generated = ToSource.to_source(ast)
parsed = generated.to_ast
again = ToSource.to_source(parsed)
unless generated == again
fail "Untransitive:\n%s\n---\n%s" % [generated, again]
end
end
unless instance_methods.include?(:expected_mutations)
let(:expected_mutations) do
mutations.map do |mutation|
case mutation
when String
mutation.to_ast
ast = mutation.to_ast
assert_transitive(ast)
ast
when Rubinius::AST::Node
assert_transitive(mutation)
mutation
else
raise

View file

@ -4,7 +4,7 @@ describe Mutant::Killer::Rspec, '.new' do
subject { object.new(strategy, mutation) }
let(:strategy) { mock('Strategy', :spec_files => ['foo']) }
let(:strategy) { mock('Strategy', :spec_files => ['foo'], :error_stream => $stderr, :output_stream => $stdout) }
let(:context) { mock('Context') }
let(:mutation) { mock('Mutation') }

View file

@ -48,6 +48,25 @@ describe Mutant::Mutator, 'define' do
mutations << 'def foo(a, b); Object.new; end'
end
it_should_behave_like 'a mutator'
end
context 'default argument' do
let(:source) { 'def foo(a = "literal"); end' }
before do
Mutant::Random.stub(:hex_string => 'random')
end
let(:mutations) do
mutations = []
mutations << 'def foo(a); end'
mutations << 'def foo(); end'
mutations << 'def foo(a = "random"); end'
mutations << 'def foo(a = nil); end'
mutations << 'def foo(a = "literal"); Object.new; end'
mutations << 'def foo(srandom = "literal"); nil; end'
end
it_should_behave_like 'a mutator'
end

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Mutant::Mutator, 'call' do
describe Mutant::Mutator, 'send' do
context 'send without arguments' do
# This could not be reproduced in a test case but happens in the mutant source code?
context 'block_given?' do