class Test::Redef

Public Class Methods

new(valid_names) click to toggle source
# File lib/test/redef.rb, line 8
def initialize(valid_names)
  @valid_names = valid_names
  reset
end
publicize_method(*methods) { || ... } click to toggle source
# File lib/test/redef.rb, line 35
def self.publicize_method(*methods)
  method_syms = []
  methods.each do |class_method_name|
    klass, method_name = parse_class_and_method(class_method_name)
    next if klass.public_instance_methods.include?(method_name)
    method_syms << [klass, method_name]
    klass.send(:public, method_name)
  end

  yield

  method_syms.each {|klass, method_name| klass.send(:private, method_name) }
end
rd(methods_to_procs, &block) click to toggle source
# File lib/test/redef.rb, line 13
def self.rd(methods_to_procs, &block)
  class_wrappers = {}
  methods_to_procs.each do |class_method_name, new_method|
    redef_class, method_name = parse_class_and_method(class_method_name)

    if !self.instance_methods_for(redef_class).include?(method_name)
      raise ArgumentError.new("No method found for #{class_method_name}")
    end

    if new_method == :empty
      new_method = proc { }
    elsif new_method == :wiretap
      new_method = proc { |*args, &block|
        method((method_name.to_s + '_redef').to_sym).call(*args, &block)
      }
    end

    (class_wrappers[redef_class] ||= {})[method_name] = [class_method_name, new_method]
  end
  swap_in_and_run(class_wrappers, block)
end

Private Class Methods

instance_methods_for(klass) click to toggle source
# File lib/test/redef.rb, line 158
def self.instance_methods_for(klass)
  return klass.instance_methods + klass.private_instance_methods + klass.protected_instance_methods
end
parse_class_and_method(class_method_name) click to toggle source
# File lib/test/redef.rb, line 92
def self.parse_class_and_method(class_method_name)
  if class_method_name.is_a?(String)
    md = class_method_name.match('^(?<class>[^.#]*)(?<sep>\.|#)(?<method>.*)$')
    method_name = md[:method].to_sym
    klass = string_to_const(md[:class])
    meta_class = (class << klass; self; end)

    redef_class = md[:sep] == '.' ? meta_class : klass
    return redef_class, method_name
  else
    klass_or_obj, method_name = class_method_name
    method_name = method_name.to_sym
    if klass_or_obj.is_a?(Class)
      meta_class = (class << klass_or_obj; self; end)
      return meta_class, method_name
    else
      return klass_or_obj.class, method_name
    end
  end
end
silent() { || ... } click to toggle source
# File lib/test/redef.rb, line 222
def self.silent
  saved = $VERBOSE
  $VERBOSE = nil

  yield
ensure
  $VERBOSE = saved
end
string_to_const(class_name) click to toggle source
# File lib/test/redef.rb, line 151
def self.string_to_const(class_name)
  const_parts = class_name.split('::')
  klass = Kernel
  const_parts.each {|p| klass = klass.const_get(p) }
  return klass
end
swap_in_and_run(klass_methods, block) click to toggle source
# File lib/test/redef.rb, line 163
def self.swap_in_and_run(klass_methods, block)
  method_hider = {}
  temporary_methods = []

  redef_state = []
  rs = self.new(klass_methods.values.map {|m| m.values.map(&:first) }.flatten(1))

  klass_methods.each do |klass, methods|
    methods.each do |method_name, method_info|
      name, method = method_info
      hider = method_name.to_s
      while klass.method_defined?(hider) || klass.private_method_defined?(hider)
        hider += '_redef'
      end
      (method_hider[klass] ||={})[method_name] = hider
      raise if hider == method_name #something has gone wrong

      klass.__send__(:alias_method, hider, method_name)
      real_redef_method_name = "__redef_new_method_#{@@redef_next_id}"
      temporary_methods << [klass, real_redef_method_name]
      @@redef_next_id += 1
      klass.__send__(:define_method, real_redef_method_name, method)
      arity = method.arity

      # wrapper to capture arguments
      klass.__send__(:define_method, method_name) do |*args, &block|
        rs.__send__(:record, name, self, args)
        if arity >= 0 && args.length > arity
          args = args.slice(0, arity)
        end
        __send__(real_redef_method_name.to_sym, *args, &block)
      end
    end
  end

  exception = nil
  begin
    ret = block.call(rs)
  rescue Exception
    exception = $!
  end

  klass_methods.each do |klass, methods|
    methods.each do |method_name, method|
      hider = method_hider[klass][method_name]
      silent { klass.__send__(:remove_method, method_name) }
      klass.__send__(:alias_method, method_name, hider)
      klass.__send__(:remove_method, hider)
    end
  end
  temporary_methods.each do |klass, temp_method_name|
    klass.__send__(:remove_method, temp_method_name)
  end

  raise exception if exception

  return ret
end

Public Instance Methods

[](method_name) click to toggle source
# File lib/test/redef.rb, line 81
def [](method_name)
  c = Class.new
  c.instance_exec(self, method_name) do |rs, rs_method_name|
    [:called, :args, :object, :reset, :called?].each do |method|
      define_method(method) { rs.__send__(method, rs_method_name) }
    end
  end
  return c.new
end
args(method_name=nil) click to toggle source
# File lib/test/redef.rb, line 57
def args(method_name=nil)
  return (lookup(@args, method_name) || []).map {|a| a[1] }
end
call_order() click to toggle source
# File lib/test/redef.rb, line 65
def call_order
  return @call_order
end
called(method_name=nil) click to toggle source
# File lib/test/redef.rb, line 49
def called(method_name=nil)
  return lookup(@called, method_name) || 0
end
called?(method_name=nil) click to toggle source
# File lib/test/redef.rb, line 53
def called?(method_name=nil)
  return called(method_name) > 0
end
object(method_name=nil) click to toggle source
# File lib/test/redef.rb, line 61
def object(method_name=nil)
  return (lookup(@args, method_name) || []).map {|a| a[0] }
end
reset(method_name=nil) click to toggle source
# File lib/test/redef.rb, line 69
def reset(method_name=nil)
  if method_name
    name = lookup_name(method_name)
    @called[name] = 0
    @args[name] = []
  else
    @called = {}
    @args = {}
    @call_order = []
  end
end

Private Instance Methods

lookup(collection, name) click to toggle source
# File lib/test/redef.rb, line 113
def lookup(collection, name)
  return collection[lookup_name(name)]
end
lookup_name(name) click to toggle source
# File lib/test/redef.rb, line 117
def lookup_name(name)
  if name.nil?
    raise ArgumentError if @valid_names.length > 1
    name = @valid_names.first
  end
  if name.instance_of?(Symbol)
    match_methods = @valid_names.select do |m|
      if m.instance_of?(String)
        m =~ /[#.]#{name}$/
      else
        m[1].to_s == name.to_s
      end
    end
    raise ArgumentError.new("Bad method name: #{name}") if match_methods.length != 1
    name = match_methods[0]
  end
  return name
end
record(name, obj, args) click to toggle source
# File lib/test/redef.rb, line 136
def record(name, obj, args)
  @called[name] ||= 0
  @called[name] += 1

  @call_order.push(name)

  copy = args
  begin
    copy = Marshal.load(Marshal.dump(args))
  rescue TypeError
  end

  (@args[name] ||= []).push([obj, copy])
end