Retry on random mutates that are the same

* This fixes problems where generated mutations have a chance
  to be the same like the mutation subject.
This commit is contained in:
Markus Schirp 2012-07-30 22:18:00 +02:00
parent 2b7e9c60c2
commit 02a726d767
9 changed files with 176 additions and 12 deletions

2
TODO
View file

@ -17,6 +17,6 @@
It can be used to make sure each literal value is touched. It can be used to make sure each literal value is touched.
* Replace nil or add "do not touch me object" to literal mutations. * Replace nil or add "do not touch me object" to literal mutations.
* Add remaining literals * Add remaining literals
* Mutate options for Regexp literals * Mutate options on Regexp literals
* Move Mutant.random_* into Mutant::Random namespace. * Move Mutant.random_* into Mutant::Random namespace.
* Use inheritable alias once (virtus,veritas,mapper,session, ...) support gem is born. * Use inheritable alias once (virtus,veritas,mapper,session, ...) support gem is born.

View file

@ -14,7 +14,9 @@ module Mutant
generator << new_self(0) generator << new_self(0)
generator << new_self(1) generator << new_self(1)
generator << new_self(-node.value) generator << new_self(-node.value)
generator << new_self(Mutant.random_fixnum) generator.generate do
new_self(Mutant.random_fixnum)
end
end end
end end
end end

View file

@ -14,7 +14,9 @@ module Mutant
generator << new_self(0.0) generator << new_self(0.0)
generator << new_self(1.0) generator << new_self(1.0)
generator << new_self(-node.value) generator << new_self(-node.value)
generator << new_self(Mutant.random_float) generator.generate do
new_self(Mutant.random_float)
end
generator << infinity generator << infinity
generator << neg_infinity generator << neg_infinity
generator << nan generator << nan

View file

@ -1,22 +1,94 @@
module Mutant module Mutant
class Mutator class Mutator
class Generator class Generator
def initialize(node,block)
@sexp,@block = node.to_sexp,block
end
# Append node to generated mutations if node does not equal orignal node
#
# @param [Rubinius::AST::Node] node
#
# @return [self]
#
# @api private
#
def append(node) def append(node)
return if same_node?(node) return self unless new?(node)
@block.call(node) @block.call(node)
end end
# FIXME: Use interhitable alias once in support gem. # FIXME: Use interhitable alias once in support gem.
alias :<< :append alias :<< :append
# Maximum amount of tries to generate a new node
MAX_TRIES = 3
# Call block until it generates a new AST node
#
# The primary use of this method is to give the random generated AST nodes
# a nice interface for retring generation when generation accidentally generated the
# same AST that is about to be mutated.
#
# @yield
# Execute block until AST node that does not equal wrapped node is generated by block
#
# @return [self]
#
# @raise [RuntimeError]
# raises RuntimeError in case no new ast node can be generated after MAX_TRIES.
#
# @api private
#
def generate
MAX_TRIES.times do
node = yield
return call(node) if new?(node)
end
raise "New AST could not be generated after #{MAX_TRIES} attempts"
end
private private
def same_node?(node) # Initialize generator
@sexp == node.to_sexp #
# @param [Rubinius::AST::Node] node
# @param [#call(node)] block
#
# @return [undefined]
#
# @api private
#
def initialize(node,block)
@sexp,@block = node.to_sexp,block
end
# Call block with node
#
# @param [Rubinius::AST::Node] node
#
# @return [self]
#
# @api private
#
def call(node)
@block.call(node)
self
end
# Check if node does not equal original node
#
# @param [Rubinius::AST::Node] node
#
# @return [true]
# returns true when node is differend from the node to be mutated
#
# @return [false]
# returns false otherwise
#
# @api private
#
def new?(node)
@sexp != node.to_sexp
end end
end end
end end

View file

@ -13,7 +13,9 @@ module Mutant
# #
def mutants(generator) def mutants(generator)
generator << new_nil generator << new_nil
generator << new_self(Mutant.random_hex_string) generator.generate do
new_self(Mutant.random_hex_string)
end
end end
end end
end end

View file

@ -11,7 +11,9 @@ module Mutant
# #
def mutants(generator) def mutants(generator)
generator << new_nil generator << new_nil
generator << new(Rubinius::AST::SymbolLiteral,Mutant.random_hex_string.to_sym) generator.generate do
new_self(Mutant.random_hex_string.to_sym)
end
end end
end end
end end

View file

@ -12,7 +12,7 @@ module Mutant
# @return [undefined] # @return [undefined]
# #
def mutants(generator) def mutants(generator)
generator << new(Rubinius::AST::NilLiteral) generator << new_ni
generator << new(Rubinius::AST::FalseLiteral) generator << new(Rubinius::AST::FalseLiteral)
end end
end end

View file

@ -93,6 +93,21 @@ describe Mutant::Mutator, '#each' do
it_should_behave_like 'a mutation enumerator method' it_should_behave_like 'a mutation enumerator method'
end end
context 'interpolated string literal (DynamicString)' do
let(:source) { '"foo#{1}bar"' }
let(:mutations) do
mutations = []
mutations << 'nil'
end
before do
Mutant.stub(:random_hex_string => random_string)
end
it_should_behave_like 'a mutation enumerator method'
end
context 'fixnum literal' do context 'fixnum literal' do
let(:source) { '10' } let(:source) { '10' }
@ -235,6 +250,32 @@ describe Mutant::Mutator, '#each' do
it_should_behave_like 'a mutation enumerator method' it_should_behave_like 'a mutation enumerator method'
end end
context 'regexp literal' do
let(:source) { '/foo/' }
let(:base_mutations) do
mutations = []
mutations << 'nil'
mutations << "/#{random_string}/"
end
before do
Mutant.stub(:random_hex_string => random_string)
end
let(:mutations) { base_mutations }
it_should_behave_like 'a mutation enumerator method'
#it 'when source is empty regexp' do
# let(:source) { '//' }
# let(:mutations) { base_mutations - [source.to_ast] }
# it_should_behave_like 'a mutation enumerator method'
#end
end
context 'block literal' do context 'block literal' do
let(:source) { "true\nfalse" } let(:source) { "true\nfalse" }

View file

@ -0,0 +1,43 @@
require 'spec_helper'
describe Mutant::Mutator::Generator,'#generate' do
subject { object.generate { node } }
class Block
attr_reader :arguments
def called?
defined?(@arguments)
end
def call(*arguments)
@arguments = arguments
end
end
let(:object) { described_class.new(wrapped_node,block) }
let(:block) { Block.new }
let(:wrapped_node) { '"foo"'.to_ast }
context 'when new AST is generated' do
let(:node) { '"bar"'.to_ast }
it 'should call block' do
subject
block.should be_called
end
it 'should call block with node' do
subject
block.arguments.should eql([node])
end
end
context 'when new AST could not be generated' do
let(:node) { '"foo"'.to_ast }
it 'should raise error' do
expect { subject }.to raise_error(RuntimeError,'New AST could not be generated after 3 attempts')
end
end
end