Jimmy
Jimmy

Reputation: 37081

Testing a REPL in Ruby with RSpec and threads

I'm using RSpec to test the behavior of a simple REPL. The REPL just echoes back whatever the input was, unless the input was "exit", in which case it terminates the loop.

To avoid hanging the test runner, I'm running the REPL method inside a separate thread. To make sure that the code in the thread has executed before I write expectations about it, I've found it necessary to include a brief sleep call. If I remove it, the tests fail intermittently because the expectations are sometimes made before the code in the thread has run.

What is a good way to structure the code and spec such that I can make expectations about the REPL's behavior deterministically, without the need for the sleep hack?

Here is the REPL class and the spec:

class REPL
  def initialize(stdin = $stdin, stdout = $stdout)
    @stdin = stdin
    @stdout = stdout
  end

  def run
    @stdout.puts "Type exit to end the session."

    loop do
      @stdout.print "$ "
      input = @stdin.gets.to_s.chomp.strip
      break if input == "exit"
      @stdout.puts(input)
    end
  end
end

describe REPL do
  let(:stdin) { StringIO.new }
  let(:stdout) { StringIO.new }
  let!(:thread) { Thread.new { subject.run } }

  subject { described_class.new(stdin, stdout) }

  # Removing this before hook causes the examples to fail intermittently
  before { sleep 0.01 }

  after { thread.kill if thread.alive? }

  it "prints a message on how to end the session" do
    expect(stdout.string).to match(/end the session/)
  end

  it "prints a prompt for user input" do
    expect(stdout.string).to match(/\$ /)
  end

  it "echoes input" do
    stdin.puts("foo")
    stdin.rewind
    expect(stdout.string).to match(/foo/)
  end
end

Upvotes: 2

Views: 823

Answers (2)

Jesse Storimer
Jesse Storimer

Reputation: 511

Instead of letting :stdout be a StringIO, you could back it by a Queue. Then when you try to read from the queue, your tests will just wait until the REPL pushes something into the queue (aka. writes to stdout).

require 'thread'

class QueueIO
  def initialize
    @queue = Queue.new
  end

  def write(str)
    @queue.push(str)
  end

  def puts(str)
    write(str + "\n")
  end

  def read
    @queue.pop
  end
end

let(:stdout) { QueueIO.new }

I just wrote this up without trying it out, and it may not be robust enough for your needs, but it gets the point across. If you use a data structure to synchronize the two threads like this, then you don't need to sleep at all. Since this removes the non-determinism, you shouldn't see the intermittent failures.

Upvotes: 1

davogones
davogones

Reputation: 7399

I've used a running? guard for situations like this. You probably can't avoid the sleep entirely, but you can avoid unnecessary sleeps.

First, add a running? method to your REPL class.

class REPL
  ...

  def running?
    !!@running
  end

  def run
    @running=true

    loop do
      ...
      if input == 'exit
        @running = false
        break
      end
      ...
    end
  end
end

Then, in your specs, sleep until the REPL is running:

describe REPL do
  ...
  before { sleep 0.01 until REPL.running? }
  ...
end

Upvotes: 0

Related Questions