Experiments in metaprogramming

I’m having a go at metaprogramming in ruby. It started out simply enough. I wanted a module which had some custom assert methods in. Here’s the test:

  def test_matches
    assert_stderr_matches(/foo/) do
      warn "foo"
    end
  end

And here are the custom assert methods.

  module AssertStderr
    def assert_stderr_matches(regex, message=nil)
      old_stderr = $stderr
      $stderr = StringIO.new
      yield
      assert_match(regex, $stderr.string, message)
    ensure
      $stderr = old_stderr
    end

    def assert_stderr_empty(message=nil)
      # More-or-less identical.
    end
  end

Two methods, the bulk of which is similar. Yuck. Refactoring time.

  module AssertStderr
    def assert_stderr_matches(regex, message=nil, &block)
      assert_match(regex, capturing_stderr(block), message)
    end

    def assert_stderr_empty(message=nil, &block)
      assert_equal("", capturing_stderr(block))
    end

    def capturing_stderr(block)
      old_stderr = $stderr
      $stderr = StringIO.new
      block.call
      return $stderr.string
    ensure
      $stderr = old_stderr
    end
  end

That’s better, but it set me wondering how far I could go down this route. What about creating an assert_stderr builder? This is what I’ve come up with so far.

  def self.assert_builder(what, &check_block)
    define_method("assert_stderr_#{what}") do |*args|
      begin
        old_stderr = $stderr
        $stderr = StringIO.new
        yield
        check_block.call($stderr.string, *args)
      ensure
        $stderr = old_stderr
      end
    end
  end

  assert_builder :matches do |stderr,regex,message|
    assert_match(regex, stderr, message)
  end

  assert_builder :empty do |stderr,message|
    assert_equal("", stderr, message)
  end

It looks reasonable, but it doesn’t work. It’s meant to create a method which takes a block and executes it. However, careful debugging (read: p) shows that the yield is actually calling the check_block parameter instead of the block that’s provided when the assert_stderr_matches method is called. At this point, I have no idea why this is happening. I’m very confused.

It doesn’t help that you can’t really google for “ruby blocks that take blocks” and get a sensible answer.

Maybe I just need to nose around the Rails source long enough. There are examples of most forms of Ruby magic hanging around in there…