Add Mutant::Zombifier

That stuff acutally works!
This commit is contained in:
Markus Schirp 2013-06-28 23:23:05 +02:00
parent ed3fb57c20
commit fa3e77f981
6 changed files with 260 additions and 178 deletions

View file

@ -4,5 +4,7 @@ gemspec
gem 'mutant', :path => '.'
gem 'unparser', :path => '../unparser'
gem 'devtools', :git => 'https://github.com/rom-rb/devtools.git'
eval(File.read(File.join(File.dirname(__FILE__),'Gemfile.devtools')))

View file

@ -114,3 +114,4 @@ require 'mutant/reporter/cli/printer/config'
require 'mutant/reporter/cli/printer/subject'
require 'mutant/reporter/cli/printer/killer'
require 'mutant/reporter/cli/printer/mutation'
require 'mutant/zombifier'

View file

@ -40,7 +40,7 @@ module Mutant
# @api private
#
def run
eval(source, TOPLEVEL_BINDING, @subject.source_path, @subject.source_line)
eval(source, TOPLEVEL_BINDING, @subject.source_path.to_s, @subject.source_line)
end
# Return source

253
lib/mutant/zombifier.rb Normal file
View file

@ -0,0 +1,253 @@
module Mutant
# Zombifier namespace
module Zombifier
# Excluded from zombification, reasons
#
# * Relies dynamic require, zombifier does not know how to recurse here (racc)
# * Unparser bug (optparse)
# * Toplevel reference/cbase nodes in code (rspec)
#
STOP = %w(
rspec
diff/lcs
parser
parser/all
parser/current
racc/parser
optparse
).to_set
# Perform self zombification
#
# @return [self]
#
# @api private
#
def self.zombify
run('mutant')
end
# Zombify gem
#
# @param [String] name
#
# @return [self]
#
# @api private
#
def self.run(name)
Gem.new(name).zombify
end
# Zombifier subject, compatible with mutants loader
class Subject < Mutant::Subject
include NodeHelpers
# Return new object
#
# @param [File]
#
# @return [Subject]
#
# @api private
#
def self.new(file)
super(file, file.node)
end
# Perform zombification on subject
#
# @return [self]
#
# @api private
#
def zombify
$stderr.puts "Zombifying #{context.source_path}"
Loader::Eval.run(zombified_root, self)
self
end
memoize :zombify
private
# Return zombified root
#
# @return [Parser::AST::Node]
#
# @api private
#
def zombified_root
s(:module, s(:const, nil, :Zombie), node)
end
end # Subject
# File containing source beeing zombified
class File
include Adamantium::Flat, Concord::Public.new(:source_path)
CACHE = {}
# Zombify contents of file
#
# @return [self]
#
# @api private
#
def zombify
subject.zombify
required_paths.each do |path|
file = File.find(path)
next unless file
file.zombify
end
self
end
# Find file
#
# @param [String] logical_name
#
# @return [File]
# if found
#
# @raise [RuntimeError]
# if file cannot be found
#
def self.find(logical_name)
return if STOP.include?(logical_name)
CACHE.fetch(logical_name) do
CACHE[logical_name] = find_uncached(logical_name)
end
end
# Find file without cache
#
# @param [String] logical_name
#
# @return [File]
# if found
#
# @return [nil]
# otherwise
#
# @api private
#
def self.find_uncached(logical_name)
file_name =
unless logical_name.end_with?('.rb')
"#{logical_name}.rb"
else
logical_name
end
$LOAD_PATH.each do |path|
path = Pathname.new(path).join(file_name)
if path.exist?
$stderr.puts "Loading #{path}"
return new(path)
end
end
$stderr.puts "Cannot find #{file_name} in $LOAD_PATH"
nil
end
# Return subject
#
# @return [Subject]
#
# @api private
#
def subject
Subject.new(self)
end
memoize :subject
# Return node
#
# @return [Parser::AST::Node]
#
# @api private
#
def node
Parser::CurrentRuby.parse(::File.read(source_path))
end
memoize :node
RECEIVER_INDEX = 0
SELECTOR_INDEX = 1
ARGUMENT_INDEX = 2..-1.freeze
# Return required paths
#
# @return [Enumerable<String>]
#
# @api private
#
def required_paths
require_nodes.map do |node|
arguments = node.children[ARGUMENT_INDEX]
unless arguments.length == 1
raise "Require node with not exactly one argument: #{node}"
end
argument = arguments.first
unless argument.type == :str
raise "Require argument is not a literal string: #{argument}"
end
argument.children.first
end
end
memoize :required_paths
private
# Return require nodes
#
# @return [Enumerable<Parser::AST::Node>]
#
# @api private
#
def require_nodes
children = node.type == :begin ? node.children : [node]
children.select do |node|
children = node.children
node.type == :send && children.at(RECEIVER_INDEX).nil? && children.at(SELECTOR_INDEX) == :require
end
end
end # File
# Gem beeing zombified
class Gem
include Adamantium::Flat, Concord.new(:name)
# Return subjects
#
# @return [Enumerable<Subject>]
#
# @api private
#
def zombify
root_file.zombify
end
memoize :zombify
private
# Return root souce file
#
# @return [File]
#
# @api private
#
def root_file
File.find(name) || raise("No root file!")
end
memoize :root_file
end # Gem
end # Zombifier
end # Mutant

View file

@ -1,8 +1,8 @@
require 'spec_helper'
describe Mutant, 'as a zombie' do
pending 'allows to create zombie from mutant' do
Zombie.setup
Zombie::Runner
specify 'it allows to create zombie from mutant' do
Mutant::Zombifier.run('mutant')
Zombie.constants.should include(:Mutant)
end
end

View file

@ -1,174 +0,0 @@
module Zombie
# Setup zombie
#
# @return [self]
#
# @api private
#
def self.setup
files.each do |path|
path = "#{File.expand_path(path, root)}.rb"
ast = File.read(path).to_ast
zombify(ast, path)
end
self
end
# Return library root directory
#
# @return [String]
#
# @api private
#
def self.root
File.expand_path('../../../lib',__FILE__)
end
private_class_method :root
class DummySubject
# Return line
#
# @return [Fixnum]
#
# @api private
#
attr_reader :source_line
# Return path
#
# @return [String]
#
# @api private
#
attr_reader :source_path
private
# Initialize object
#
# @param [String] path
# @param [Fixnum] line
#
# @return [undefined]
#
# @api private
#
def initialize(path, line)
@source_path, @source_line = path, line
end
end
# Replace Mutant with Zombie namespace
#
# @param [Rubinius::AST::Node]
#
# @api private
#
# @return [undefined]
#
def self.zombify(root, path)
node = find_mutant(root)
unless node
raise "unable to find mutant in AST from: #{path.inspect}"
end
name = node.name
node.name = Rubinius::AST::ModuleName.new(name.line, :Zombie)
scope = node.body
unless scope.kind_of?(Rubinius::AST::EmptyBody)
node.body = Rubinius::AST::ModuleScope.new(scope.line, node.name, scope.body)
end
::Mutant::Loader::Eval.run(root, DummySubject.new(path, 1))
end
private_class_method :zombify
# Find mutant module in AST
#
# @param [Rubinius::AST::Node]
#
# @return [Rubinius::AST::Node]
#
def self.find_mutant(root)
if is_mutant?(root)
return root
end
unless root.kind_of?(Rubinius::AST::Block)
raise "Cannot find mutant in: #{root.class}"
end
root.array.each do |node|
return node if is_mutant?(node)
end
nil
end
private_class_method :find_mutant
# Test if node is mutant module
#
# @param [Rubinius::AST::Node]
#
# @return [true]
# returns true if node is the mutant module
#
# @return [false]
# returns false otherwise
#
# @api private
#
def self.is_mutant?(node)
node.kind_of?(Rubinius::AST::Module) && is_mutant_name?(node.name)
end
private_class_method :is_mutant?
# Test if node is mutant module name
#
# @param [Rubinius::AST::ModuleName]
#
# @return [true]
# returns true if node is the mutant module name
#
# @return [false]
# returns false otherwise
#
# @api private
#
def self.is_mutant_name?(node)
node.name == :Mutant
end
private_class_method :is_mutant_name?
# Return all library files the mutant is made of.
#
# @return [Array<String>]
#
# @api private
#
# FIXME:
# Yeah looks very ugly but im currently to exited to do a cleanup.
#
def self.files
block = File.read('lib/mutant.rb').to_ast
files = block.array.select do |node|
node.class == Rubinius::AST::SendWithArguments &&
node.receiver.class == Rubinius::AST::Self &&
node.name == :require
end.map do |node|
arguments = node.arguments.array
raise unless arguments.one?
argument = arguments.first
raise unless argument.class == Rubinius::AST::StringLiteral
argument.string
end.select do |file|
file =~ /\Amutant/
end
end
private_class_method :files
end