diff --git a/bin/mutant b/bin/mutant index bbdc27c9..9c114ccb 100755 --- a/bin/mutant +++ b/bin/mutant @@ -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 diff --git a/lib/mutant.rb b/lib/mutant.rb index 61d37b26..2bb59658 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -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' diff --git a/lib/mutant/require_highjack.rb b/lib/mutant/require_highjack.rb index 78a5df5b..20e69d20 100644 --- a/lib/mutant/require_highjack.rb +++ b/lib/mutant/require_highjack.rb @@ -25,8 +25,6 @@ module Mutant desinfect end - private - # Infect kernel with highjack # # @return [self] diff --git a/lib/mutant/zombifier.rb b/lib/mutant/zombifier.rb index fe552fca..1f1ce69f 100644 --- a/lib/mutant/zombifier.rb +++ b/lib/mutant/zombifier.rb @@ -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] - # - # @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] - # - # @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] - # - # @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 diff --git a/lib/mutant/zombifier/file.rb b/lib/mutant/zombifier/file.rb new file mode 100644 index 00000000..4af70203 --- /dev/null +++ b/lib/mutant/zombifier/file.rb @@ -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 diff --git a/spec/integration/mutant/zombie_spec.rb b/spec/integration/mutant/zombie_spec.rb index a852dbdd..386032c3 100644 --- a/spec/integration/mutant/zombie_spec.rb +++ b/spec/integration/mutant/zombie_spec.rb @@ -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