Fix zombifier to use the require highjack

This commit is contained in:
Markus Schirp 2014-04-04 14:14:24 +00:00
parent 7eed61c117
commit 1b2bb54852
6 changed files with 156 additions and 247 deletions

View file

@ -10,7 +10,7 @@ require 'mutant'
namespace =
if ARGV.include?('--zombie')
$stderr.puts('Running mutant zombified!')
Mutant::Zombifier.zombify
Mutant.zombify
Zombie::Mutant
else
Mutant

View file

@ -25,6 +25,18 @@ module Mutant
EMPTY_STRING = ''.freeze
# The frozen empty array used within mutant
EMPTY_ARRAY = [].freeze
# Perform self zombification
#
# @return [self]
#
# @api private
#
def self.zombify
Zombifier.run('mutant', :Zombie)
self
end
end # Mutant
require 'mutant/version'
@ -133,3 +145,4 @@ require 'mutant/reporter/cli/printer/subject'
require 'mutant/reporter/cli/printer/killer'
require 'mutant/reporter/cli/printer/mutation'
require 'mutant/zombifier'
require 'mutant/zombifier/file'

View file

@ -25,8 +25,6 @@ module Mutant
desinfect
end
private
# Infect kernel with highjack
#
# @return [self]

View file

@ -2,264 +2,113 @@
module Mutant
# Zombifier namespace
module Zombifier
class Zombifier
include Adamantium::Flat, Concord.new(:namespace)
# Excluded from zombification, reasons
#
# * Relies dynamic require, zombifier does not know how to recurse (racc)
# * Unparser bug (optparse)
# * Toplevel reference/cbase nodes in code (rspec)
# * Creates useless toplevel modules that get vendored under ::Zombie (set)
#
IGNORE = %w(
set
rspec
diff/lcs
diff/lcs/hunk
unparser
parser
parser/all
parser/current
racc/parser
optparse
).to_set
# Excluded from zombification
IGNORE = [
# Unparser is not performant enough (does some backtracking!) for generated lexer.rb
'parser',
'parser/all',
'parser/current',
# Wierd constant definitions / guards.
'diff/lcs',
'diff/lcs/hunk',
# Mix beteen constants defined in .so and .rb files
# Cannot be deterministically namespaced from ruby
# without dynamically recompiling openssl ;)
'openssl',
# Constant propagation errors
'thread_safe'
].to_set.freeze
# Perform self zombification
# Initialize object
#
# @param [Symbol] namespace
#
# @return [undefined]
#
# @api private
#
def initialize(namespace)
@namespace = namespace
@zombified = Set.new(IGNORE)
end
# Perform zombification of target library
#
# @param [String] logical_name
# @param [Symbol] namespace
#
# @api private
#
def self.run(logical_name, namespace)
new(namespace).run(logical_name)
end
# Run zombifier
#
# @param [String] logical_name
#
# @return [undefined]
#
# @api private
#
def run(logical_name)
highjack = RequireHighjack.new(Kernel, method(:require))
highjack.infect
require(logical_name)
end
# Require file in zombie namespace
#
# @param [String] logical_name
#
# @return [self]
#
# @api private
#
def self.zombify
run('mutant')
def require(logical_name)
return if @zombified.include?(logical_name)
@zombified << logical_name
file = find(logical_name)
file.zombify(namespace) if file
self
end
# Zombify gem
private
# Find file without cache
#
# @param [String] name
# @param [String] logical_name
#
# @return [self]
# @return [File]
# if found
#
# @return [nil]
# otherwise
#
# @api private
#
def self.run(name)
Gem.new(name).zombify
self
def find(logical_name)
file_name =
case File.extname(logical_name)
when '.so'
return
when '.rb'
logical_name
else
"#{logical_name}.rb"
end
$LOAD_PATH.each do |path|
path = Pathname.new(path).join(file_name)
return File.new(path) if path.file?
end
$stderr.puts "Cannot find file #{file_name} in $LOAD_PATH"
nil
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.call(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
memoize :zombify
# Find file
#
# @param [String] logical_name
#
# @return [File]
# if found
#
# @raise [RuntimeError]
# if file cannot be found
#
# @api private
#
def self.find(logical_name)
return if IGNORE.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 =
if logical_name.end_with?('.rb')
logical_name
else
"#{logical_name}.rb"
end
$LOAD_PATH.each do |path|
path = Pathname.new(path).join(file_name)
if path.file?
return new(path)
end
end
$stderr.puts "Cannot find file #{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) or raise 'No root file!'
end
memoize :root_file
end # Gem
end # Zombifier
end # Mutant

View file

@ -0,0 +1,49 @@
module Mutant
class Zombifier
# File containing source beeing zombified
class File
include NodeHelpers, Adamantium::Flat, Concord::Public.new(:path)
# Zombify contents of file
#
# @return [self]
#
# @api private
#
def zombify(namespace)
$stderr.puts("Zombifying #{path.to_s}")
eval(
Unparser.unparse(namespaced_node(namespace)),
TOPLEVEL_BINDING,
path.to_s
)
self
end
private
# Return node
#
# @return [Parser::AST::Node]
#
# @api private
#
def node
Parser::CurrentRuby.parse(path.read)
end
# Return namespaced root
#
# @param [Symbol] namespace
#
# @return [Parser::AST::Node]
#
# @api private
#
def namespaced_node(namespace)
s(:module, s(:const, nil, namespace), node)
end
end # File
end # Zombifier
end # Mutant

View file

@ -3,8 +3,8 @@
require 'spec_helper'
describe Mutant, 'as a zombie' do
pending 'it allows to create zombie from mutant' do
Mutant::Zombifier.run('mutant')
specify 'it allows to create zombie from mutant' do
expect { Mutant.zombify }.to change { !!defined?(Zombie) }.from(false).to(true)
expect(Zombie.constants).to include(:Mutant)
end
end