Merge branch 'master' into warning-filter

Conflicts:
	lib/mutant.rb
This commit is contained in:
Markus Schirp 2014-04-22 18:02:07 +00:00
commit 877e4540d4
32 changed files with 370 additions and 233 deletions

View file

@ -4,4 +4,5 @@ AllCops:
Excludes:
- 'Gemfile.devtools'
- 'vendor/**'
- 'tmp/**'
- 'benchmarks/**'

View file

@ -1,9 +1,11 @@
# v0.5.11 2014-04-07
# v0.5.11 2014-04-xx
Changes:
* Fix crash on while and until without body
* Better require highjack based zombifier
* Do not mutate nthref $1 to gvar $0
* Use faster duplicate guarding hashing AST::Node intances
# v0.5.10 2014-04-06

View file

@ -6,7 +6,5 @@ gem 'mutant', path: '.'
gemspec name: 'mutant'
gem 'morpher', git: 'https://github.com/mbj/morpher.git'
gem 'devtools', git: 'https://github.com/rom-rb/devtools.git'
eval_gemfile 'Gemfile.devtools'

View file

@ -1,3 +1,15 @@
name: mutant
namespace: Mutant
zombify: true
expect_coverage: 64.99
ignore_subjects:
# Mutation causes infinite runtime
- Mutant::Runner.lookup
# Suboptimal test selection stragegy (will be fixed soon) causes timeouts on CI
- Mutant::Zombifier*
- Mutant::Reporter*
- Mutant::CLI*
- Mutant.singleton_subclass_instance
- Mutant.symbolset
# Executing this has undefined behavior with the zombifier
- Mutant.zombify

View file

@ -54,6 +54,7 @@ NestedIterators:
- Mutant::Reporter::CLI::Printer::Config::Runner#generic_stats
- Mutant::RequireHighjack#infect
- Mutant::RequireHighjack#desinfect
- Parser::Lexer#self.new
max_allowed_nesting: 1
ignore_iterators: []
NilCheck:
@ -126,7 +127,7 @@ UncommunicativeVariableName:
- !ruby/regexp /^.$/
- !ruby/regexp /[0-9]$/
- !ruby/regexp /[A-Z]/
accept: []
accept: ['force_utf32']
UnusedParameters:
enabled: true
exclude: []

View file

@ -11,6 +11,7 @@ require 'digest/sha1'
require 'inflecto'
require 'parser'
require 'parser/current'
require 'parser_extensions'
require 'unparser'
require 'ice_nine'
require 'diff/lcs'
@ -37,12 +38,48 @@ module Mutant
self
end
# Return a frozen set of symbols from string enumerable
#
# @param [Enumerable<String>]
#
# @return [Set<Symbol>]
#
# @api private
#
def self.symbolset(strings)
strings.map(&:to_sym).to_set.freeze
end
private_class_method :symbolset
# Define instance of subclassed superclass as constant
#
# @param [Class] superclass
# @param [Symbol] name
#
# @return [self]
#
# @api private
#
def self.singleton_subclass_instance(name, superclass, &block)
klass = Class.new(superclass) do
def inspect
self.class.name
end
define_singleton_method(:name) do
"#{superclass.name}::#{name}".freeze
end
end
klass.class_eval(&block)
superclass.const_set(name, klass.new)
self
end
end # Mutant
require 'mutant/version'
require 'mutant/cache'
require 'mutant/node_helpers'
require 'mutant/singleton_methods'
require 'mutant/warning_filter'
require 'mutant/constants'
require 'mutant/random'
@ -91,6 +128,8 @@ require 'mutant/mutator/node/zsuper'
require 'mutant/mutator/node/restarg'
require 'mutant/mutator/node/send'
require 'mutant/mutator/node/send/binary'
require 'mutant/mutator/node/send/attribute_assignment'
require 'mutant/mutator/node/send/index'
require 'mutant/mutator/node/when'
require 'mutant/mutator/node/define'
require 'mutant/mutator/node/mlhs'
@ -103,6 +142,7 @@ require 'mutant/mutator/node/case'
require 'mutant/mutator/node/splat'
require 'mutant/mutator/node/resbody'
require 'mutant/mutator/node/rescue'
require 'mutant/mutator/node/match_current_line'
require 'mutant/config'
require 'mutant/loader'
require 'mutant/context'

View file

@ -2,19 +2,6 @@
module Mutant
# Return a frozen set of symbols from string enumerable
#
# @param [Enumerable<String>]
#
# @return [Set<Symbol>]
#
# @api private
#
def self.symbolset(strings)
strings.map(&:to_sym).to_set.freeze
end
private_class_method :symbolset
# Set of nodes that cannot be on the LHS of an assignment
NOT_ASSIGNABLE = symbolset %w(
int float str dstr class module self

View file

@ -33,18 +33,6 @@ module Mutant
end
private_class_method :handle
# Return identity of object (for deduplication)
#
# @param [Object] object
#
# @return [Object]
#
# @api private
#
def self.identity(object)
object
end
# Return input
#
# @return [Object]
@ -92,7 +80,7 @@ module Mutant
# @api private
#
def new?(object)
!@seen.include?(identity(object))
!@seen.include?(object)
end
# Add object to guarded values
@ -104,19 +92,7 @@ module Mutant
# @api private
#
def guard(object)
@seen << identity(object)
end
# Return identity for input
#
# @param [Object] input
#
# @return [Object]
#
# @api private
#
def identity(input)
self.class.identity(input)
@seen << object
end
# Dispatch node generations

View file

@ -9,18 +9,6 @@ module Mutant
class Node < self
include AbstractType, NodeHelpers, Unparser::Constants
# Return identity of node
#
# @param [Parser::AST::Node] node
#
# @return [String]
#
# @api private
#
def self.identity(node)
Unparser.unparse(node)
end
# Define named child
#
# @param [Symbol] name
@ -58,6 +46,10 @@ module Mutant
children.each_with_index.drop(names.length)
end
define_method(:remaining_children_indices) do
children.each_index.drop(names.length)
end
define_method(:remaining_children) do
children.drop(names.length)
end

View file

@ -13,7 +13,7 @@ module Mutant
:ensure, :redo, :defined?, :regopt, :retry, :arg_expr,
:kwrestarg, :kwoptarg, :kwarg, :undef, :module, :empty,
:alias, :for, :xstr, :back_ref, :class,
:sclass, :match_with_lvasgn, :match_current_line, :while_post,
:sclass, :match_with_lvasgn, :while_post,
:until_post, :preexe, :postexe, :iflipflop, :eflipflop, :kwsplat,
:shadowarg
)

View file

@ -33,7 +33,7 @@ module Mutant
#
def mutate_condition
emit_condition_mutations
emit_self(n_not(condition), if_branch, else_branch)
emit_self(n_not(condition), if_branch, else_branch) unless condition.type == :match_current_line
emit_self(N_TRUE, if_branch, else_branch)
emit_self(N_FALSE, if_branch, else_branch)
end

View file

@ -31,11 +31,11 @@ module Mutant
# @api private
#
def dispatch
emit_nil
emit_nil unless parent_type == :match_current_line
children.each_with_index do |child, index|
mutate_child(index) unless child.type == :str
end
emit_self(s(:str, EMPTY_STRING), options)
emit_self(options)
emit_self(s(:str, NULL_REGEXP_SOURCE), options)
end

View file

@ -0,0 +1,27 @@
module Mutant
class Mutator
class Node
# Emitter for perl style match current line node
class MatchCurrentLine < self
handle :match_current_line
children :regexp
private
# Emit mutants
#
# @return [undefined]
#
# @api private
#
def dispatch
emit_nil
emit_regexp_mutations
end
end # MatchCurrentLine
end # Node
end # Mutator
end # Mutant

View file

@ -42,7 +42,7 @@ module Mutant
def mutate_name
prefix = MAP.fetch(node.type)
Mutator::Util::Symbol.each(name, self) do |name|
emit_name(prefix + name.to_s)
emit_name(:"#{prefix}#{name}")
end
end

View file

@ -19,7 +19,9 @@ module Mutant
# @api private
#
def dispatch
emit_number(number - 1)
unless number.equal?(1)
emit_number(number - 1)
end
emit_number(number + 1)
end

View file

@ -8,18 +8,6 @@ module Mutant
handle :rescue
# Return identity
#
# @param [Parser::AST::Node] node
#
# @return [String]
#
# @api private
#
def self.identity(node)
super(NodeHelpers.s(:kwbegin, node))
end
end # Rescue
end # Node
end # Mutator

View file

@ -22,40 +22,6 @@ module Mutant
INDEX_ASSIGN = :[]=
ASSIGN_SUFFIX = '='.freeze
# Base mutator for index operations
class Index < self
# Mutator for index references
class Reference < self
# Perform dispatch
#
# @return [undefined]
#
# @api private
#
def dispatch
emit(receiver)
end
end # Reference
# Mutator for index assignments
class Assign < self
# Perform dispatch
#
# @return [undefined]
#
# @api private
#
def dispatch
emit(receiver)
end
end # Assign
end # Index
private
# Perform dispatch
@ -65,6 +31,7 @@ module Mutant
# @api private
#
def dispatch
emit_nil
case selector
when INDEX_REFERENCE
run(Index::Reference)
@ -73,7 +40,6 @@ module Mutant
else
non_index_dispatch
end
emit_nil
end
# Perform non index dispatch
@ -86,6 +52,8 @@ module Mutant
case
when binary_operator?
run(Binary)
when attribute_assignment?
run(AttributeAssignment)
else
normal_dispatch
end
@ -173,7 +141,6 @@ module Mutant
# @api private
#
def mutate_arguments
return if arguments.empty?
emit_self(receiver, selector)
remaining_children_with_index.each do |node, index|
mutate_child(index)
@ -213,7 +180,10 @@ module Mutant
# @api private
#
def emit_implicit_self
if receiver.type == :self && !KEYWORDS.include?(selector) && !attribute_assignment?
if receiver.type == :self &&
!KEYWORDS.include?(selector) &&
!attribute_assignment? &&
!OP_ASSIGN.include?(parent_type)
emit_receiver(nil)
end
end

View file

@ -0,0 +1,51 @@
# encoding: utf-8
module Mutant
class Mutator
class Node
class Send
# Mutator for sends that correspond to an attribute assignment
class AttributeAssignment < self
private
# Emit mutations
#
# @return [undefined]
#
# @api private
#
def dispatch
normal_dispatch
emit_attribute_read
end
# Mutate arguments
#
# @return [undefined]
#
# @api private
#
def mutate_arguments
remaining_children_indices.each do |index|
mutate_child(index)
end
end
# Emit attribute read
#
# @return [undefined]
#
# @api private
#
def emit_attribute_read
emit_self(receiver, selector.to_s[0..-2].to_sym)
end
end # AttributeAssignment
end # Send
end # Node
end # Mutator
end # Mutant

View file

@ -0,0 +1,43 @@
# encoding: UTF-8
module Mutant
class Mutator
class Node
class Send
# Base mutator for index operations
class Index < self
# Mutator for index references
class Reference < self
# Perform dispatch
#
# @return [undefined]
#
# @api private
#
def dispatch
emit(receiver)
end
end # Reference
# Mutator for index assignments
class Assign < self
# Perform dispatch
#
# @return [undefined]
#
# @api private
#
def dispatch
emit(receiver)
end
end # Assign
end # Index
end # Send
end # Node
end # Mutator
end # Mutant

View file

@ -18,13 +18,13 @@ module Mutant
module_function :s
NAN =
s(:send, s(:float, 0.0), :/, s(:args, s(:float, 0.0)))
s(:send, s(:float, 0.0), :/, s(:float, 0.0))
INFINITY =
s(:send, s(:float, 1.0), :/, s(:args, s(:float, 0.0)))
s(:send, s(:float, 1.0), :/, s(:float, 0.0))
NEW_OBJECT =
s(:send, s(:const, s(:cbase), :Object), :new)
NEGATIVE_INFINITY =
s(:send, s(:float, -1.0), :/, s(:args, s(:float, 0.0)))
s(:send, s(:float, -1.0), :/, s(:float, 0.0))
RAISE = s(:send, nil, :raise)

View file

@ -1,30 +0,0 @@
# encoding: utf-8
# Singleton methods are defined here so zombie can pick them up
module Mutant
# Define instance of subclassed superclass as constant
#
# @param [Class] superclass
# @param [Symbol] name
#
# @return [self]
#
# @api private
#
def self.singleton_subclass_instance(name, superclass, &block)
klass = Class.new(superclass) do
def inspect
self.class.name
end
define_singleton_method(:name) do
"#{superclass.name}::#{name}".freeze
end
end
klass.class_eval(&block)
superclass.const_set(name, klass.new)
self
end
end # Mutant

View file

@ -26,9 +26,9 @@ module Mutant
# @api private
#
def initialize(namespace)
@namespace = namespace
@zombified = Set.new
@highjack = RequireHighjack.new(Kernel, method(:require))
super(namespace)
end
# Perform zombification of target library

25
lib/parser_extensions.rb Normal file
View file

@ -0,0 +1,25 @@
# Monkeypatch to silence warnings in parser
#
# Will be removed once https://github.com/whitequark/parser/issues/145 is solved.
# Parser namespace
module Parser
# Monkeypatched lexer
class Lexer
# Return new lexer
#
# @return [Lexer]
#
# @api private
#
def self.new(*arguments)
super.tap do |instance|
instance.instance_eval do
@force_utf32 = false
end
end
end
end # Lexer
end # Parser

View file

@ -24,18 +24,19 @@ Gem::Specification.new do |gem|
gem.required_ruby_version = '>= 1.9.3'
gem.add_runtime_dependency('parser', '~> 2.1')
gem.add_runtime_dependency('ast', '~> 2.0')
gem.add_runtime_dependency('diff-lcs', '~> 1.2')
gem.add_runtime_dependency('morpher', '~> 0.2.1')
gem.add_runtime_dependency('morpher', '~> 0.2.3')
gem.add_runtime_dependency('procto', '~> 0.0.2')
gem.add_runtime_dependency('abstract_type', '~> 0.0.7')
gem.add_runtime_dependency('unparser', '~> 0.1.10')
gem.add_runtime_dependency('unparser', '~> 0.1.12')
gem.add_runtime_dependency('ice_nine', '~> 0.11.0')
gem.add_runtime_dependency('adamantium', '~> 0.2.0')
gem.add_runtime_dependency('memoizable', '~> 0.4.2')
gem.add_runtime_dependency('equalizer', '~> 0.0.9')
gem.add_runtime_dependency('inflecto', '~> 0.0.2')
gem.add_runtime_dependency('anima', '~> 0.2.0')
gem.add_runtime_dependency('concord', '~> 0.1.4')
gem.add_runtime_dependency('concord', '~> 0.1.5')
gem.add_development_dependency('bundler', '~> 1.3', '>= 1.3.5')
end

View file

@ -1,52 +1,12 @@
# encoding: utf-8
class Subject
include Equalizer.new(:source)
Undefined = Object.new.freeze
attr_reader :source
def self.coerce(input)
case input
when Parser::AST::Node
new(input)
when String
new(Parser::CurrentRuby.parse(input))
else
raise
end
end
def to_s
"#{@node.inspect}\n#{@source}"
end
def initialize(node)
source = Unparser.unparse(node)
@node, @source = node, source
end
def assert_transitive!
generated = Unparser.generate(@node)
parsed = Parser::CurrentRuby.parse(generated)
again = Unparser.generate(parsed)
unless generated == again
# mostly an unparser bug!
fail sprintf("Untransitive:\n%s\n---\n%s", generated, again)
end
self
end
end
# encoding: UTF-8
shared_examples_for 'a mutator' do
subject { object.each(node) { |item| yields << item } }
subject { object.each(node, &yields.method(:<<)) }
let(:yields) { [] }
let(:object) { described_class }
unless instance_methods.map(&:to_s).include?('node')
unless instance_methods.include?(:node)
let(:node) { parse(source) }
end
@ -57,42 +17,32 @@ shared_examples_for 'a mutator' do
it { should be_instance_of(to_enum.class) }
let(:expected_mutations) do
mutations.map(&Subject.method(:coerce))
def coerce(input)
case input
when String
Parser::CurrentRuby.parse(input)
when Parser::AST::Node
input
else
raise
end
end
let(:generated_mutations) do
def normalize(node)
Unparser::Preprocessor.run(node)
end
let(:expected_mutations) do
mutations.map(&method(:coerce)).map(&method(:normalize))
end
it 'generates the expected mutations' do
generated_mutations = subject.map(&method(:normalize))
generated = subject.map { |node| Subject.new(node) }
verifier = MutationVerifier.new(node, expected_mutations, generated_mutations)
missing = expected_mutations - generated
unexpected = generated - expected_mutations
message = []
if missing.any?
message << sprintf('Missing mutations (%i):', missing.length)
message.concat(missing)
end
if unexpected.any?
message << sprintf('Unexpected mutations (%i):', unexpected.length)
message.concat(unexpected)
end
if message.any?
message = sprintf(
"Original:\n%s\n%s\n-----\n%s",
generate(node),
node.inspect,
message.join("\n-----\n")
)
fail message
unless verifier.success?
fail verifier.error_report
end
end
end

View file

@ -21,8 +21,10 @@ if ENV['COVERAGE'] == 'true'
end
end
require 'equalizer'
require 'concord'
require 'adamantium'
require 'devtools/spec_helper'
require 'unparser/cli'
require 'mutant'
$LOAD_PATH << File.join(TestApp.root, 'lib')
@ -39,7 +41,7 @@ module ParserHelper
end
def parse(string)
Parser::CurrentRuby.parse(string)
Unparser::Preprocessor.run(Parser::CurrentRuby.parse(string))
end
end

View file

@ -0,0 +1,95 @@
# encoding: UTF-8
class MutationVerifier
include Adamantium::Flat, Concord.new(:original_node, :expected, :generated)
# Test if mutation was verified successfully
#
# @return [Boolean]
#
# @api private
#
def success?
unparser.success? && missing.empty? && unexpected.empty?
end
# Return error report
#
# @return [String]
#
# @api private
#
def error_report
unless unparser.success?
return unparser.report
end
mutation_report
end
private
# Return unexpected mutationso
#
# @return [Array<Parser::AST::Node>]
#
# @api private
#
def unexpected
generated - expected
end
memoize :unexpected
# Return mutation report
#
# @return [String]
#
# @api private
#
def mutation_report
message = ['Original:', original_node.inspect]
if missing.any?
message << 'Missing mutations:'
message << missing.map(&method(:format_mutation)).join("\n-----\n")
end
if unexpected.any?
message << 'Unexpected mutations:'
message << unexpected.map(&method(:format_mutation)).join("\n-----\n")
end
message.join("\n======\n")
end
# Format mutation
#
# @return [String]
#
# @api private
#
def format_mutation(node)
[
node.inspect,
Unparser.unparse(node)
].join("\n")
end
# Return missing mutationso
#
# @return [Array<Parser::AST::Node>]
#
# @api private
#
def missing
expected - generated
end
memoize :missing
# Return unparser verifier
#
# @return [Unparser::CLI::Source]
#
# @api private
#
def unparser
Unparser::CLI::Source::Node.new(Unparser::Preprocessor.run(original_node))
end
memoize :unparser
end # MutationVerifier

View file

@ -3,16 +3,16 @@
require 'spec_helper'
describe Mutant::Mutator::Node::Generic, 'match_current_line' do
let(:source) { 'true if //' }
let(:source) { 'true if /foo/' }
let(:mutations) do
mutations = []
mutations << 'false if //'
mutations << 'nil if //'
mutations << 'false if /foo/'
mutations << 'true if //'
mutations << 'nil if /foo/'
mutations << 'true if true'
mutations << 'true if false'
mutations << 'true if nil'
mutations << s(:if, s(:send, s(:match_current_line, s(:regexp, s(:regopt))), :!), s(:true), nil)
mutations << 'true if /a\A/'
mutations << 'nil'
end

View file

@ -60,10 +60,13 @@ describe Mutant::Mutator::Node::NamedValue::Access, 'mutations' do
mutants = []
mutants << 'a = nil; nil'
mutants << 'a = nil'
mutants << 'a'
mutants << 'a = ::Object.new; a'
mutants << 'srandom = nil; a'
mutants << 'nil; a'
# TODO: fix invalid AST
# These ASTs are not valid and should NOT be emitted
# Mutations of lvarasgn need to be special cased to avoid this.
mutants << s(:begin, s(:lvasgn, :srandom, s(:nil)), s(:lvar, :a))
mutants << s(:begin, s(:nil), s(:lvar, :a))
mutants << s(:lvar, :a)
end
it_should_behave_like 'a mutator'

View file

@ -4,8 +4,8 @@ require 'spec_helper'
describe Mutant::Mutator, 'nthref' do
context '$1' do
let(:source) { '$1' }
let(:mutations) { ['$2', '$0'] }
let(:source) { '$1' }
let(:mutations) { ['$2'] }
it_should_behave_like 'a mutator'
end

View file

@ -13,10 +13,12 @@ describe Mutant::Mutator::Node::Generic, 'op_asgn' do
mutations << '@a.b += 2'
mutations << '@a.b += 0'
mutations << '@a.b += nil'
mutations << '@a += 1'
mutations << '@a.b += 5'
mutations << 'nil.b += 1'
mutations << 'nil'
# TODO: fix invalid AST
# This should not get emitted as invalid AST with valid unparsed source
mutations << s(:op_asgn, s(:ivar, :@a), :+, s(:int, 1))
end
before do

View file

@ -63,7 +63,6 @@ describe Mutant::Mutator, 'send' do
let(:mutations) do
mutations = []
mutations << 'foo ||= expression'
mutations << 'self.foo ||= nil'
mutations << 'nil.foo ||= expression'
mutations << 'nil'