Add Mutant::Zombifier
That stuff acutally works!
This commit is contained in:
parent
ed3fb57c20
commit
fa3e77f981
6 changed files with 260 additions and 178 deletions
2
Gemfile
2
Gemfile
|
@ -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')))
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
253
lib/mutant/zombifier.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue