MTJ Hax

MiniTest version of RSpec any_instance

Posted in code, ruby, ruby on rails by mtjhax on August 30, 2013

Using MiniTest in Rails apps, I have been missing the RSpec any_instance method:

MyClass.any_instance.stubs(:a_method)

There are other ways to stub in MiniTest but any_instance is convenient and expressive, so I wrote my own quickie version based on aliasing. I call it all_instances to avoid any problems if also using RSpec. Drop this in a file in test/support and make sure it is included in your test_helper.rb:

# this allows a MiniTest test to stub out a method on all instances of a class
# examples:
#
# MyClass.all_instances.stub(:foo, 'bar') do
#   MyClass.new.foo.must_equal 'bar'
# end
#
# MyClass.all_instances.stub(:foo, 'bar')
# MyClass.new.foo.must_equal 'bar'
# MyClass.all_instances.unstub(:foo, 'bar')

class AllInstances
  def initialize(klass)
    @klass = klass
    @@new_method_names ||= {}
  end

  def stub(method_name, return_value)
    @@new_method_names[method_name] = "_original_#{method_name}".to_sym
    @klass.send(:alias_method, @@new_method_names[method_name], method_name)
    @klass.send(:define_method, method_name, ->(*args){ return_value })
    if block_given?
      begin
        yield
      ensure
        unstub(method_name)
      end
    end
  end

  def unstub(method_name)
    @klass.send(:remove_method, method_name)
    @klass.send(:alias_method, method_name, @@new_method_names[method_name])
    @@new_method_names[method_name] = nil
  end
end

class Class
  def all_instances
    AllInstances.new(self)
  end
end

Cute Ruby trick: split a hash into two hashes with a block

Posted in code, ruby by mtjhax on June 21, 2013
foo = { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 }
bar = foo.select {|k, v| foo.delete(k) || true if k >= 'c' }
# foo == {"a"=>1, "b"=>2}
# bar == {"c"=>3, "d"=>4}

So why use || true? Consider this example:

foo = { 'a' => 1, 'b' => 2, 'c' => nil, 'd' => 4 }
bar = foo.select {|k, v| foo.delete(k) if k >= 'c' }
# foo == {"a"=>1, "b"=>2}
# bar == {"d"=>4}

foo.delete(k) returns the value that was deleted, which in the case of key ‘c’ is the value nil. The Hash#select method treats nil as meaning do not select this element for the result set, so the ‘c’ entry is deleted from the original hash and not included in the result hash. Adding || true returns true even if the value in question evaluates to false or nil.

Ruby 1.8 strftime works differently on Linux and Windows

Posted in code, ruby, ruby on rails by mtjhax on December 27, 2010

Running Ruby 1.8 on Linux, cygwin, Mac, any POSIX environment:

now = Time.now
=> Mon Dec 27 18:28:28 -0500 2010
now.strftime("%l") # that's a lowercase "L"
=> " 6"

Under Windows using the exact same Ruby version and patch level:

now = Time.now
=> Mon Dec 27 18:28:28 -0500 2010
now.strftime("%l") # that's a lowercase "L"
=> ""

What? It turns out in Ruby 1.8 they cheated by simply calling the standard C strftime() function to implement Ruby’s strftime method. Because different compilers implement strftime() differently, compiling it under Windows results in a different set of supported % directives. Specifically, it looks like under Windows only the basic ANSI C version is implemented, but under POSIX compilers you get a mix of directives defined by ANSI and other standards like Gnu and Single Unix Specification. The “%l” directive was not even listed in the Ruby 1.8 docs — it worked by accident.

The good news is in Rails 1.9 they appear to have implemented their own strftime for consistency, instead of handing it off to the C compiler. If you need to fix this in the meantime, I provide the following monkey patch (which I simply place in my Rails config/initializers folder on any Windows developer machine):

# monkey patch Ruby strftime methods to implement
# %l directive that is missing in Windows Ruby
#
# as a monkey patch, just use this on your Windows
# development boxes to bridge the gap, it's not
# recommended for production
#
# notes:
# - you may want to add support for other missing directives
# - check your RUBY_PLATFORM and make sure it is in the regexp below
#
if RUBY_PLATFORM =~ /mingw32|mingw64|mswin32|mswin64/

  class Time
    alias_method :original_strftime, :strftime
    def strftime(fmt)
      hour12 = "%2d" % ((hour + 11) % 12 + 1)
      original_strftime(fmt.gsub(/%l/, hour12))
    end
  end
 
  class Date
    alias_method :original_strftime, :strftime
    def strftime(fmt)
      hour12 = "%2d" % ((hour + 11) % 12 + 1)
      original_strftime(fmt.gsub(/%l/, hour12))
    end
  end

end
Tagged with: , ,