Reduce zombifier
* And add a full specification
This commit is contained in:
parent
a54e686255
commit
c56ecdb51a
14 changed files with 399 additions and 251 deletions
18
bin/mutant
18
bin/mutant
|
@ -10,7 +10,23 @@ require 'mutant'
|
||||||
namespace =
|
namespace =
|
||||||
if ARGV.include?('--zombie')
|
if ARGV.include?('--zombie')
|
||||||
$stderr.puts('Running mutant zombified!')
|
$stderr.puts('Running mutant zombified!')
|
||||||
Mutant.zombify
|
Mutant::Zombifier.call(
|
||||||
|
namespace: :Zombie,
|
||||||
|
load_path: $LOAD_PATH,
|
||||||
|
kernel: Kernel,
|
||||||
|
pathname: Pathname,
|
||||||
|
require_highjack: Mutant::RequireHighjack.method(:call).to_proc.curry.call(Kernel),
|
||||||
|
root_require: 'mutant',
|
||||||
|
includes: %w[
|
||||||
|
mutant
|
||||||
|
unparser
|
||||||
|
morpher
|
||||||
|
adamantium
|
||||||
|
equalizer
|
||||||
|
anima
|
||||||
|
concord
|
||||||
|
]
|
||||||
|
)
|
||||||
Zombie::Mutant
|
Zombie::Mutant
|
||||||
else
|
else
|
||||||
Mutant
|
Mutant
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
---
|
---
|
||||||
threshold: 18
|
threshold: 18
|
||||||
total_score: 1240
|
total_score: 1227
|
||||||
|
|
|
@ -46,8 +46,7 @@ NestedIterators:
|
||||||
- Mutant::Mutator::Util::Array::Element#dispatch
|
- Mutant::Mutator::Util::Array::Element#dispatch
|
||||||
- Mutant::Mutator::Node::Resbody#mutate_captures
|
- Mutant::Mutator::Node::Resbody#mutate_captures
|
||||||
- Mutant::Mutator::Node::Arguments#emit_argument_mutations
|
- Mutant::Mutator::Node::Arguments#emit_argument_mutations
|
||||||
- Mutant::RequireHighjack#infect
|
- Mutant::RequireHighjack#self.call
|
||||||
- Mutant::RequireHighjack#disinfect
|
|
||||||
- Mutant::Selector::Expression#call
|
- Mutant::Selector::Expression#call
|
||||||
- Mutant::Parallel::Master#run
|
- Mutant::Parallel::Master#run
|
||||||
- Parser::Lexer#self.new
|
- Parser::Lexer#self.new
|
||||||
|
|
|
@ -24,11 +24,8 @@ Thread.abort_on_exception = true
|
||||||
|
|
||||||
# Library namespace
|
# Library namespace
|
||||||
module Mutant
|
module Mutant
|
||||||
# The frozen empty string used within mutant
|
EMPTY_STRING = ''.freeze
|
||||||
EMPTY_STRING = ''.freeze
|
EMPTY_ARRAY = [].freeze
|
||||||
# The frozen empty array used within mutant
|
|
||||||
EMPTY_ARRAY = [].freeze
|
|
||||||
|
|
||||||
SCOPE_OPERATOR = '::'.freeze
|
SCOPE_OPERATOR = '::'.freeze
|
||||||
|
|
||||||
# Test if CI is detected via environment
|
# Test if CI is detected via environment
|
||||||
|
@ -41,17 +38,6 @@ module Mutant
|
||||||
ENV.key?('CI')
|
ENV.key?('CI')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Perform self zombification
|
|
||||||
#
|
|
||||||
# @return [self]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def self.zombify
|
|
||||||
Zombifier.run('mutant', :Zombie)
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
# Define instance of subclassed superclass as constant
|
# Define instance of subclassed superclass as constant
|
||||||
#
|
#
|
||||||
# @param [Class] superclass
|
# @param [Class] superclass
|
||||||
|
@ -216,7 +202,6 @@ require 'mutant/reporter/cli/printer/test_result'
|
||||||
require 'mutant/reporter/cli/tput'
|
require 'mutant/reporter/cli/tput'
|
||||||
require 'mutant/reporter/cli/format'
|
require 'mutant/reporter/cli/format'
|
||||||
require 'mutant/zombifier'
|
require 'mutant/zombifier'
|
||||||
require 'mutant/zombifier/file'
|
|
||||||
|
|
||||||
module Mutant
|
module Mutant
|
||||||
# Reopen class to initialize constant to avoid dep circle
|
# Reopen class to initialize constant to avoid dep circle
|
||||||
|
|
|
@ -1,62 +1,23 @@
|
||||||
module Mutant
|
module Mutant
|
||||||
# Require highjack
|
# Require highjack
|
||||||
class RequireHighjack
|
module RequireHighjack
|
||||||
include Concord.new(:target, :callback)
|
|
||||||
|
|
||||||
# Return original method
|
# Install require callback
|
||||||
|
#
|
||||||
|
# @param [Module] target
|
||||||
|
# @param [#call] callback
|
||||||
#
|
#
|
||||||
# @return [#call]
|
# @return [#call]
|
||||||
|
# the original implementation on singleton
|
||||||
#
|
#
|
||||||
# @api private
|
# @api private
|
||||||
#
|
#
|
||||||
attr_reader :original
|
def self.call(target, callback)
|
||||||
|
target.method(:require).tap do
|
||||||
# Run block with highjacked require
|
target.module_eval do
|
||||||
#
|
define_method(:require, &callback)
|
||||||
# @return [self]
|
public :require
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def run
|
|
||||||
infect
|
|
||||||
yield
|
|
||||||
self
|
|
||||||
ensure
|
|
||||||
disinfect
|
|
||||||
end
|
|
||||||
|
|
||||||
# Infect kernel with highjack
|
|
||||||
#
|
|
||||||
# @return [self]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def infect
|
|
||||||
callback = @callback
|
|
||||||
@original = target.method(:require)
|
|
||||||
target.module_eval do
|
|
||||||
undef :require
|
|
||||||
define_method(:require) do |logical_name|
|
|
||||||
callback.call(logical_name)
|
|
||||||
end
|
end
|
||||||
module_function :require
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Imperfectly disinfect kernel from highjack
|
|
||||||
#
|
|
||||||
# @return [self]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def disinfect
|
|
||||||
original = @original
|
|
||||||
target.module_eval do
|
|
||||||
undef :require
|
|
||||||
define_method(:require) do |logical_name|
|
|
||||||
original.call(logical_name)
|
|
||||||
end
|
|
||||||
module_function :require
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
module Mutant
|
module Mutant
|
||||||
# Zombifier namespace
|
# Zombifier namespace
|
||||||
class Zombifier
|
class Zombifier
|
||||||
include Adamantium::Flat, Concord.new(:namespace)
|
include Anima.new(
|
||||||
|
:includes,
|
||||||
|
:namespace,
|
||||||
|
:load_path,
|
||||||
|
:kernel,
|
||||||
|
:require_highjack,
|
||||||
|
:root_require,
|
||||||
|
:pathname
|
||||||
|
)
|
||||||
|
|
||||||
# Excluded into zombification
|
include AST::Sexp
|
||||||
includes = %w[
|
|
||||||
mutant
|
|
||||||
unparser
|
|
||||||
morpher
|
|
||||||
adamantium
|
|
||||||
equalizer
|
|
||||||
anima
|
|
||||||
concord
|
|
||||||
]
|
|
||||||
|
|
||||||
INCLUDES = %r{\A#{Regexp.union(includes)}(?:/.*)?\z}.freeze
|
LoadError = Class.new(::LoadError)
|
||||||
|
|
||||||
# Initialize object
|
# Initialize object
|
||||||
#
|
#
|
||||||
|
@ -24,37 +23,28 @@ module Mutant
|
||||||
#
|
#
|
||||||
# @api private
|
# @api private
|
||||||
#
|
#
|
||||||
def initialize(namespace)
|
def initialize(*)
|
||||||
|
super
|
||||||
|
@includes = %r{\A#{Regexp.union(includes)}(?:/.*)?\z}
|
||||||
@zombified = Set.new
|
@zombified = Set.new
|
||||||
@highjack = RequireHighjack.new(Kernel, method(:require))
|
|
||||||
super(namespace)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Perform zombification of target library
|
def self.call(*args)
|
||||||
#
|
new(*args).__send__(:call)
|
||||||
# @param [String] logical_name
|
|
||||||
# @param [Symbol] namespace
|
|
||||||
#
|
|
||||||
# @return [self]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def self.run(logical_name, namespace)
|
|
||||||
new(namespace).run(logical_name)
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
# Run zombifier
|
# Run zombifier
|
||||||
#
|
#
|
||||||
# @param [String] logical_name
|
|
||||||
#
|
|
||||||
# @return [undefined]
|
# @return [undefined]
|
||||||
#
|
#
|
||||||
# @api private
|
# @api private
|
||||||
#
|
#
|
||||||
def run(logical_name)
|
def call
|
||||||
@highjack.infect
|
@original = require_highjack.call(method(:require))
|
||||||
require(logical_name)
|
require(root_require)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Test if logical name is subjected to zombification
|
# Test if logical name is subjected to zombification
|
||||||
|
@ -64,25 +54,77 @@ module Mutant
|
||||||
# @api private
|
# @api private
|
||||||
#
|
#
|
||||||
def include?(logical_name)
|
def include?(logical_name)
|
||||||
!@zombified.include?(logical_name) && INCLUDES =~ logical_name
|
!@zombified.include?(logical_name) && @includes =~ logical_name
|
||||||
end
|
end
|
||||||
|
|
||||||
# Require file in zombie namespace
|
# Require file in zombie namespace
|
||||||
#
|
#
|
||||||
# @param [#to_s] logical_name
|
# @param [#to_s] logical_name
|
||||||
#
|
#
|
||||||
# @return [self]
|
# @return [undefined]
|
||||||
#
|
#
|
||||||
# @api private
|
# @api private
|
||||||
#
|
#
|
||||||
def require(logical_name)
|
def require(logical_name)
|
||||||
logical_name = logical_name.to_s
|
logical_name = logical_name.to_s
|
||||||
@highjack.original.call(logical_name)
|
@original.call(logical_name)
|
||||||
return unless include?(logical_name)
|
return unless include?(logical_name)
|
||||||
@zombified << logical_name
|
@zombified << logical_name
|
||||||
file = File.find(logical_name)
|
zombify(find(logical_name))
|
||||||
file.zombify(namespace) if file
|
end
|
||||||
self
|
|
||||||
|
# Find file by logical path
|
||||||
|
#
|
||||||
|
# @param [String] logical_name
|
||||||
|
#
|
||||||
|
# @return [File]
|
||||||
|
#
|
||||||
|
# @raise [LoadError]
|
||||||
|
# otherwise
|
||||||
|
#
|
||||||
|
# @api private
|
||||||
|
#
|
||||||
|
def find(logical_name)
|
||||||
|
file_name = "#{logical_name}.rb"
|
||||||
|
|
||||||
|
load_path.each do |path|
|
||||||
|
path = pathname.new(path).join(file_name)
|
||||||
|
return path if path.file?
|
||||||
|
end
|
||||||
|
|
||||||
|
fail LoadError, "Cannot find file #{file_name.inspect} in load path"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Zombify contents of file
|
||||||
|
#
|
||||||
|
# Probably the 2nd valid use of eval ever. (First one is inserting mutants!).
|
||||||
|
#
|
||||||
|
# @param [Pathname] source_path
|
||||||
|
#
|
||||||
|
# @return [undefined]
|
||||||
|
#
|
||||||
|
# @api private
|
||||||
|
#
|
||||||
|
# rubocop:disable Lint/Eval
|
||||||
|
#
|
||||||
|
def zombify(source_path)
|
||||||
|
kernel.eval(
|
||||||
|
Unparser.unparse(namespaced_node(source_path)),
|
||||||
|
TOPLEVEL_BINDING,
|
||||||
|
source_path.to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return namespaced root
|
||||||
|
#
|
||||||
|
# @param [Symbol] namespace
|
||||||
|
#
|
||||||
|
# @return [Parser::AST::Node]
|
||||||
|
#
|
||||||
|
# @api private
|
||||||
|
#
|
||||||
|
def namespaced_node(source_path)
|
||||||
|
s(:module, s(:const, nil, namespace), Parser::CurrentRuby.parse(source_path.read))
|
||||||
end
|
end
|
||||||
|
|
||||||
end # Zombifier
|
end # Zombifier
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
module Mutant
|
|
||||||
class Zombifier
|
|
||||||
# File containing source being zombified
|
|
||||||
class File
|
|
||||||
include Adamantium::Flat, Concord::Public.new(:path), AST::Sexp
|
|
||||||
|
|
||||||
# Zombify contents of file
|
|
||||||
#
|
|
||||||
# @return [self]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
# Probably one of the only valid uses of eval.
|
|
||||||
#
|
|
||||||
# rubocop:disable Lint/Eval
|
|
||||||
#
|
|
||||||
def zombify(namespace)
|
|
||||||
$stderr.puts("Zombifying #{path}")
|
|
||||||
eval(
|
|
||||||
Unparser.unparse(namespaced_node(namespace)),
|
|
||||||
TOPLEVEL_BINDING,
|
|
||||||
path.to_s
|
|
||||||
)
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
# Find file by logical path
|
|
||||||
#
|
|
||||||
# @param [String] logical_name
|
|
||||||
#
|
|
||||||
# @return [File]
|
|
||||||
# if found
|
|
||||||
#
|
|
||||||
# @return [nil]
|
|
||||||
# otherwise
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def self.find(logical_name)
|
|
||||||
file_name = expand_file_name(logical_name)
|
|
||||||
|
|
||||||
$LOAD_PATH.each do |path|
|
|
||||||
path = Pathname.new(path).join(file_name)
|
|
||||||
return new(path) if path.file?
|
|
||||||
end
|
|
||||||
|
|
||||||
$stderr.puts "Cannot find file #{file_name} in $LOAD_PATH"
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return expanded file name
|
|
||||||
#
|
|
||||||
# @param [String] logical_name
|
|
||||||
#
|
|
||||||
# @return [nil]
|
|
||||||
# if no expansion is possible
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def self.expand_file_name(logical_name)
|
|
||||||
case ::File.extname(logical_name)
|
|
||||||
when '.so'
|
|
||||||
return
|
|
||||||
when '.rb'
|
|
||||||
logical_name
|
|
||||||
else
|
|
||||||
"#{logical_name}.rb"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
private_class_method :expand_file_name
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Return node
|
|
||||||
#
|
|
||||||
# @return [Parser::AST::Node]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def node
|
|
||||||
Parser::CurrentRuby.parse(path.read, path.to_s)
|
|
||||||
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
|
|
|
@ -1,6 +0,0 @@
|
||||||
RSpec.describe 'as a zombie', mutant: false do
|
|
||||||
specify 'it allows to create zombie from mutant' do
|
|
||||||
expect { Mutant.zombify }.to change { defined?(Zombie) }.from(nil).to('constant')
|
|
||||||
expect(Zombie.constants).to include(:Mutant)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -20,6 +20,7 @@ end
|
||||||
|
|
||||||
require 'tempfile'
|
require 'tempfile'
|
||||||
require 'concord'
|
require 'concord'
|
||||||
|
require 'anima'
|
||||||
require 'adamantium'
|
require 'adamantium'
|
||||||
require 'devtools/spec_helper'
|
require 'devtools/spec_helper'
|
||||||
require 'unparser/cli'
|
require 'unparser/cli'
|
||||||
|
|
60
spec/support/file_system.rb
Normal file
60
spec/support/file_system.rb
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
module MutantSpec
|
||||||
|
class FileState
|
||||||
|
DEFAULTS = IceNine.deep_freeze(
|
||||||
|
file: false,
|
||||||
|
contents: nil,
|
||||||
|
requires: []
|
||||||
|
)
|
||||||
|
|
||||||
|
include Adamantium, Anima.new(*DEFAULTS.keys)
|
||||||
|
|
||||||
|
def self.new(attributes = DEFAULTS)
|
||||||
|
super(DEFAULTS.merge(attributes))
|
||||||
|
end
|
||||||
|
|
||||||
|
DOES_NOT_EXIST = new
|
||||||
|
|
||||||
|
alias_method :file?, :file
|
||||||
|
end # FileState
|
||||||
|
|
||||||
|
class FakePathname
|
||||||
|
include Adamantium, Concord.new(:file_system, :pathname)
|
||||||
|
|
||||||
|
def join(*arguments)
|
||||||
|
self.class.new(
|
||||||
|
file_system,
|
||||||
|
pathname.join(*arguments)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read
|
||||||
|
state.contents
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
pathname.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def file?
|
||||||
|
state.file?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def state
|
||||||
|
file_system.state(pathname.to_s)
|
||||||
|
end
|
||||||
|
end # Pathname
|
||||||
|
|
||||||
|
class FileSystem
|
||||||
|
include Adamantium, Concord.new(:file_states)
|
||||||
|
|
||||||
|
def state(filename)
|
||||||
|
file_states.fetch(filename, FileState::DOES_NOT_EXIST)
|
||||||
|
end
|
||||||
|
|
||||||
|
def path(filename)
|
||||||
|
FakePathname.new(self, Pathname.new(filename))
|
||||||
|
end
|
||||||
|
end # FileSystem
|
||||||
|
end # MutantSpec
|
82
spec/support/ruby_vm.rb
Normal file
82
spec/support/ruby_vm.rb
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
module MutantSpec
|
||||||
|
# Not a real VM, just kidding. It connects the require / eval triggers
|
||||||
|
# require semantics Zombifier relies on in a way we can avoid having to
|
||||||
|
# mock around everyhwere to test every detail.
|
||||||
|
#
|
||||||
|
# rubocop:disable LineLength
|
||||||
|
class RubyVM
|
||||||
|
include Concord.new(:expected_events)
|
||||||
|
|
||||||
|
# An event being observed by the VM handlers
|
||||||
|
class EventObservation
|
||||||
|
include Concord::Public.new(:type, :payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
# An event being expected, can advance the VM
|
||||||
|
class EventExpectation
|
||||||
|
include AbstractType, Anima.new(:expected_payload, :trigger_requires)
|
||||||
|
|
||||||
|
DEFAULTS = IceNine.deep_freeze(trigger_requires: [])
|
||||||
|
|
||||||
|
def initialize(attributes)
|
||||||
|
super(DEFAULTS.merge(attributes))
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle(vm, observation)
|
||||||
|
unless match?(observation)
|
||||||
|
fail "Unexpected event observation: #{observation.inspect}, expected #{inspect}"
|
||||||
|
end
|
||||||
|
|
||||||
|
trigger_requires.each(&vm.method(:require))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
abstract_method :advance_vm
|
||||||
|
|
||||||
|
def match?(observation)
|
||||||
|
observation.type.eql?(self.class) && observation.payload.eql?(expected_payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Expectation and advance on require calls
|
||||||
|
class Require < self
|
||||||
|
end
|
||||||
|
|
||||||
|
# Expectation and advance on eval calls
|
||||||
|
class Eval < self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# A fake implementation of Kernel#require
|
||||||
|
def require(logical_name)
|
||||||
|
handle_event(EventObservation.new(EventExpectation::Require, logical_name: logical_name))
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
# A fake implementation of Kernel#eval
|
||||||
|
def eval(source, binding, location)
|
||||||
|
handle_event(
|
||||||
|
EventObservation.new(
|
||||||
|
EventExpectation::Eval,
|
||||||
|
binding: binding,
|
||||||
|
source: source,
|
||||||
|
source_location: location
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
# Test if VM events where fully processed
|
||||||
|
def done?
|
||||||
|
expected_events.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def handle_event(observation)
|
||||||
|
fail "Unexpected event: #{observation.type} / #{observation.payload}" if expected_events.empty?
|
||||||
|
|
||||||
|
expected_events.slice!(0).handle(self, observation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end # MutantSpec
|
|
@ -1,50 +1,47 @@
|
||||||
RSpec.describe Mutant::RequireHighjack do
|
RSpec.describe Mutant::RequireHighjack do
|
||||||
let(:object) { described_class.new(target, highjacked_calls.method(:push)) }
|
|
||||||
|
|
||||||
let(:highjacked_calls) { [] }
|
let(:highjacked_calls) { [] }
|
||||||
let(:require_calls) { [] }
|
let(:require_calls) { [] }
|
||||||
|
|
||||||
let(:target) do
|
let(:target_module) do
|
||||||
acc = require_calls
|
acc = require_calls
|
||||||
Module.new do
|
Module.new do
|
||||||
define_method(:require, &acc.method(:<<))
|
define_method(:require, &acc.method(:<<))
|
||||||
|
|
||||||
module_function :require
|
module_function :require
|
||||||
|
public :require
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#run' do
|
def target_require(logical_name)
|
||||||
let(:block) { -> {} }
|
Object.new.extend(target_module).require(logical_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.call' do
|
||||||
let(:logical_name) { double('Logical Name') }
|
let(:logical_name) { double('Logical Name') }
|
||||||
|
|
||||||
subject do
|
def apply
|
||||||
object.run(&block)
|
described_class.call(target_module, highjacked_calls.method(:<<))
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'require calls before run' do
|
it 'returns the original implementation from singleton' do
|
||||||
it 'does not highjack anything' do
|
expect { apply.call(logical_name) }
|
||||||
target.require(logical_name)
|
.to change { require_calls }
|
||||||
expect(require_calls).to eql([logical_name])
|
.from([])
|
||||||
expect(highjacked_calls).to eql([])
|
.to([logical_name])
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'require calls during run' do
|
it 'does highjack target object #requires calls' do
|
||||||
let(:block) { -> { target.require(logical_name) } }
|
apply
|
||||||
|
expect { target_require(logical_name) }
|
||||||
it 'does highjack the calls' do
|
.to change { highjacked_calls }
|
||||||
expect { subject }.to change { highjacked_calls }.from([]).to([logical_name])
|
.from([])
|
||||||
expect(require_calls).to eql([])
|
.to([logical_name])
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'require calls after run' do
|
it 'does not call original require' do
|
||||||
|
apply
|
||||||
it 'does not the calls anything' do
|
expect { target_require(logical_name) }
|
||||||
subject
|
.not_to change { require_calls }.from([])
|
||||||
target.require(logical_name)
|
|
||||||
expect(require_calls).to eql([logical_name])
|
|
||||||
expect(highjacked_calls).to eql([])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
120
spec/unit/mutant/zombifier_spec.rb
Normal file
120
spec/unit/mutant/zombifier_spec.rb
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
RSpec.describe Mutant::Zombifier do
|
||||||
|
let(:root_require) { Pathname.new('project') }
|
||||||
|
|
||||||
|
let(:pathname) do
|
||||||
|
instance_double(::Pathname.singleton_class)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:require_highjack) do
|
||||||
|
lambda do |block|
|
||||||
|
original = ruby_vm.method(:require)
|
||||||
|
allow(ruby_vm).to receive(:require, &block)
|
||||||
|
original
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:options) do
|
||||||
|
{
|
||||||
|
load_path: %w[a b],
|
||||||
|
includes: %w[project bar],
|
||||||
|
namespace: :Zombie,
|
||||||
|
require_highjack: require_highjack,
|
||||||
|
root_require: root_require,
|
||||||
|
pathname: pathname,
|
||||||
|
kernel: ruby_vm
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ruby_vm) do
|
||||||
|
MutantSpec::RubyVM.new(
|
||||||
|
[
|
||||||
|
MutantSpec::RubyVM::EventExpectation::Require.new(
|
||||||
|
expected_payload: {
|
||||||
|
logical_name: 'project'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
MutantSpec::RubyVM::EventExpectation::Eval.new(
|
||||||
|
expected_payload: {
|
||||||
|
binding: TOPLEVEL_BINDING,
|
||||||
|
source: "module Zombie\n module Project\n end\nend",
|
||||||
|
source_location: 'a/project.rb'
|
||||||
|
},
|
||||||
|
trigger_requires: %w[foo bar]
|
||||||
|
),
|
||||||
|
MutantSpec::RubyVM::EventExpectation::Require.new(
|
||||||
|
expected_payload: {
|
||||||
|
logical_name: 'foo'
|
||||||
|
},
|
||||||
|
trigger_requires: %w[bar]
|
||||||
|
),
|
||||||
|
MutantSpec::RubyVM::EventExpectation::Require.new(
|
||||||
|
expected_payload: {
|
||||||
|
logical_name: 'bar'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
MutantSpec::RubyVM::EventExpectation::Eval.new(
|
||||||
|
expected_payload: {
|
||||||
|
binding: TOPLEVEL_BINDING,
|
||||||
|
source: "module Zombie\n module Bar\n end\nend",
|
||||||
|
source_location: 'b/bar.rb'
|
||||||
|
},
|
||||||
|
trigger_requires: %w[]
|
||||||
|
),
|
||||||
|
MutantSpec::RubyVM::EventExpectation::Require.new(
|
||||||
|
expected_payload: {
|
||||||
|
logical_name: 'bar'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:require_effects) do
|
||||||
|
{
|
||||||
|
'project' => { requires: [] }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:file_entries) do
|
||||||
|
{
|
||||||
|
'a/project.rb' => { file: true, contents: 'module Project; end' },
|
||||||
|
'b/bar.rb' => { file: true, contents: 'module Bar; end' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:file_system) do
|
||||||
|
MutantSpec::FileSystem.new(
|
||||||
|
Hash[
|
||||||
|
file_entries.map { |key, attributes| [key, MutantSpec::FileState.new(attributes)] }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.call' do
|
||||||
|
def apply
|
||||||
|
described_class.call(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(pathname).to receive(:new, &file_system.method(:path))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns self' do
|
||||||
|
expect(apply).to be(described_class)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'consumes walks the VM through expected steps' do
|
||||||
|
expect { apply }.to change { ruby_vm.done? }.from(false).to(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when zombifier require fails' do
|
||||||
|
let(:file_entries) do
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises zombifier specific load error' do
|
||||||
|
expect { apply }.to raise_error(described_class::LoadError, 'Cannot find file "project.rb" in load path')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,15 +13,6 @@ RSpec.describe Mutant do
|
||||||
it { should be(result) }
|
it { should be(result) }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.zombify' do
|
|
||||||
subject { object.zombify }
|
|
||||||
|
|
||||||
it 'calls the zombifier' do
|
|
||||||
expect(Mutant::Zombifier).to receive(:run).with('mutant', :Zombie)
|
|
||||||
subject
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.singleton_subclass_instance' do
|
describe '.singleton_subclass_instance' do
|
||||||
subject { object.singleton_subclass_instance(name, superclass, &block) }
|
subject { object.singleton_subclass_instance(name, superclass, &block) }
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue