One of the small joys of computer programming is reading some code and thinking aha, that’s nice.

I recently encountered this feeling of aha while reading the docs for the playwright-ruby-client’s set up code for Capybara which described a mismatch between the Playwright and RSpec interface:

The challenge is that Playwright.create and playwright.chromium.launch are block-scoped APIs — the browser shuts down when the block exits. RSpec has before(:suite) and after(:suite) but no around(:suite). A Fiber bridges the gap: start! resumes the fiber until it yields the browser back, and stop! resumes it again so both blocks exit cleanly.

I’ll get to that code in a moment, but to set up the explanation: I have written a lot of code that converts start- and stop-like methods into a block interface. That looks something like this:

def with_resource
  resource = ResourceLibrary.start!
  yield resource
ensure
  ResourceLibrary.stop!
end

# and then used it like
with_resource do |resource|
  my_code_does_stuff_with resource
end

…but I can’t remember a time when I’ve gone the other way: when a library only provides a block-style interface and I needed to convert it to distinct start and stop methods.

If I hadn’t seen this playwright-ruby-client documentation, I’d have probably imagined doing it with a Thread and Queues to smuggle the resource out of a block. Something like this:

class ThreadedResourceWrapper
  attr_reader :resource

  def initialize
    @control_queue = Queue.new
    @resource_queue = Queue.new
  end

  def start!
    @thread = Thread.new do
      ResourceLibrary.open do |the_resource|
        @resource_queue.push(the_resource)
        @control_queue.pop # Pause the thread
      end
    end

    @resource = @resource_queue.pop # Smuggle out the resource
  end

  def stop!
    @control_queue.push(:stop) # Resume the thread to end the block
    @thread.join
    @resource = nil
  end
end

Instead, I’m impressed with using a simple Fiber (blocking, no scheduler) to effectively do the same thing, but without the (slight) overhead of a separate thread:

class FiberResourceWrapper
  attr_reader :resource

  def start
    @fiber = Fiber.new do
      ResourceLibrary.open do |the_resource|
        Fiber.yield the_resource
      end
    end

    @resource = @fiber.resume # Resume #1: Run the fiber to smuggle out the resource
  end

  def stop
    @fiber.resume # Resume #2: resume the fiber to end the block
    @resource = nil
  end
end

Of light commentary: The usage of two Queue objects in the ThreadedResourceWrapper is slightly complicated for managing the execution flow… and I also find the Fiber resume behavior to be slightly confusing (or at least require extra attention) because a Fiber has to be explicitly resume’d after initialization to run at all, unlike a Thread which starts executing immediately (well, implicitly but concurrently)

And lastly, for fun, I imagined how I might use this new Fiber knowledge to patch an around_suite callback into RSpec so I didn’t have to wrap every individual resource:

module RSpecAroundSuite
  def around_suite(&block)
    fiber = nil

    RSpec.configure do |config|
      config.before(:suite) do
        fiber = Fiber.new do
          run_suite = -> { Fiber.yield }
          run_suite.define_singleton_method(:run) { call } # allow `suite.run` to be analogous to `example.run`

          block.call(run_suite)
        end

        fiber.resume
      end

      config.after(:suite) do
        fiber.resume
      end
    end
  end
end

RSpec.extend(RSpecAroundSuite)

RSpec.around_suite do |suite|
  ResourceLibrary.open do |the_resource|
    $resource = the_resource # assign it to somewhere accessible
    suite.run
  end
end