Search code examples
rubytestingspawn

How to stop a process from within the tests, when testing a never-ending process?


I am developing a long-running program in Ruby. I am writing some integration tests for this. These tests need to kill or stop the program after starting it; otherwise the tests hang.

For example, with a file bin/runner

#!/usr/bin/env ruby
while true do
  puts "Hello World"
  sleep 10
end

The (integration) test would be:

class RunReflectorTest < TestCase
  test "it prints a welcome message over and over" do
    out, err = capture_subprocess_io do
      system "bin/runner"
    end
    assert_empty err
    assert_includes out, "Hello World"
  end
end

Only, obviously, this will not work; the test starts and never stops, because the system call never ends.

How should I tackle this? Is the problem in system itself, and would Kernel#spawn provide a solution? If so, how? Somehow the following keeps the out empty:

class RunReflectorTest < TestCase
  test "it prints a welcome message over and over" do
    out, err = capture_subprocess_io do
      pid = spawn "bin/runner"
      sleep 2
      Process.kill pid
    end
    assert_empty err
    assert_includes out, "Hello World"
  end
end

. This direction also seems like it will cause a lot of timing-issues (and slow tests). Ideally, a reader would follow the stream of STDOUT and let the test pass as soon as the string is encountered and then immediately kill the subprocess. I cannot find how to do this with Process.


Solution

  • With the answer from CodeGnome on how to use Timeout::timeout and the answer from andyconhin on how to redirect Process::spawn IO, I came up with two Minitest helpers that can be used as follows:

    it "runs a deamon" do
      wait_for(timeout: 2) do
        wait_for_spawned_io(regexp: /Hello World/, command: ["bin/runner"])
      end
    end
    

    The helpers are:

    def wait_for(timeout: 1, &block)
      Timeout::timeout(timeout) do
        yield block
      end
    rescue Timeout::Error
      flunk "Test did not pass within #{timeout} seconds"
    end
    
    def wait_for_spawned_io(regexp: //, command: [])
      buffer = ""
    
      begin
        read_pipe, write_pipe = IO.pipe
        pid = Process.spawn(command.shelljoin, out: write_pipe, err: write_pipe)
    
        loop do
          buffer << read_pipe.readpartial(1000)
          break if regexp =~ buffer
        end
      ensure
        read_pipe.close
        write_pipe.close
        Process.kill("INT", pid)
      end
    
      buffer
    end
    

    These can be used in a test which allows me to start a subprocess, capture the STDOUT and as soon as it matches the test Regular Expression, it passes, else it will wait 'till timeout and flunk (fail the test).

    The loop will capture output and pass the test once it sees matching output. It uses a IO.pipe because that is most transparant for subprocesses (and their children) to write to.

    I doubt this will work on Windows. And it needs some cleaning up of the wait_for_spawned_io which is doing slightly too much IMO. Antoher problem is that the Process.kill('INT') might not reach the children which are orphaned but still running after this test has ran. I need to find a way to ensure the entire subtree of processes is killed.