Skip to content

Instantly share code, notes, and snippets.

@tkareine
Created September 21, 2011 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tkareine/1232111 to your computer and use it in GitHub Desktop.
Save tkareine/1232111 to your computer and use it in GitHub Desktop.
Test global state changing code with fork
# coding: utf-8
# Test global state changing code with fork
#
# * You should avoid having global state altogether
# * That said, if you cannot avoid it, for one reason or another,
# you can execute state changing code in a child process
# * Fork a child process, execute state changing code there
# * The forked child process has its own memory space, separate
# from the parent process
# * Communicate test data to the parent via pipe or other external
# means (such as a file)
# * You could use IO.popen and its variants, but then you cannot
# execute arbitrary Ruby code for initialization (IO.popen does
# fork-exec)
#
# The code is available at <https://gist.github.com/1232111>.
#
# by Tuomas Kareinen, Reaktor
# <tuomas.kareinen@reaktori.fi>, <tkareine@gmail.com>
class MyApp
def initialize(state)
$my_app_state = state
end
def global_state=(state)
$my_app_state = state
end
def global_state
$my_app_state
end
def wip
raise 'Implement me'
end
end
if $0 == __FILE__
require 'minitest/autorun'
require_relative 'pfork'
class ForkTestDemo < MiniTest::Unit::TestCase
include PFork
def setup
@app = MyApp.new('foo')
end
def test_changing_global_state
fork_and_wait { @app.global_state = 'bar' }
assert_equal 'foo', @app.global_state
end
def test_changing_global_state_and_communicate_via_file
require 'tempfile'
Tempfile.open('fork_test_demo') do |f|
fork_and_wait do
@app.global_state = 'bar'
f.write @app.global_state
end
assert_equal 'bar', File.read(f.path)
end
end
def test_changing_global_state_and_communicate_via_stdout
fun = lambda do
@app.global_state = 'bar'
$stdout.write @app.global_state
end
pfork(fun) do |_, _, stdout, _|
assert_equal 'bar', stdout.read
end
end
def test_exception_propagates_to_parent_process
fun = lambda { @app.wip }
err = assert_raises(RuntimeError) { pfork(fun) }
assert_equal 'Implement me', err.to_s
end
def test_system_time_change
require 'date'
require 'timecop'
xmas_eve = Date.civil 2010, 12, 24
fun = lambda do
Timecop.freeze xmas_eve
$stdout.write Time.now
# Do whatever that depends on current time
end
pfork(fun) do |_, _, stdout, _|
assert_equal xmas_eve, Date.parse(stdout.read)
end
end
private
def fork_and_wait(&block)
pid = fork(&block)
Process.waitpid pid
end
end
end
# coding: utf-8
module PFork
extend self
# Call function +fun+ in a child process, giving access to the child process
# id and capturing STDIN, STDOUT, and STDERR of the function call.
#
# Adapted from stdlib's Open3::popen3 and
# {Open4::popen4}[https://github.com/ahoward/open4] by Ara T. Howard, but
# without using double forking and fork-exec'ing.
def pfork(fun, &block)
pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe
verbose = $VERBOSE
begin
$VERBOSE = nil
cid = fork do
pw.last.close
STDIN.reopen pw.first
pw.first.close
pr.first.close
STDOUT.reopen pr.last
pr.last.close
pe.first.close
STDERR.reopen pe.last
pe.last.close
ps.first.close
STDOUT.sync = STDERR.sync = true
begin
fun.call
exit_status = 0
rescue SystemExit => e
# Make it seem to the caller that calling Kernel#exit in +fun+ kills
# the child process normally. Kernel#exit! bypasses this rescue
# block.
exit_status = e.status
rescue Exception => e
Marshal.dump e, ps.last
ps.last.flush
exit_status = 42 # won't be seen by the caller
ensure
ps.last.close unless ps.last.closed?
exit! exit_status
end
end
ensure
$VERBOSE = verbose
end
[ pw.first, pr.last, pe.last, ps.last ].each { |fd| fd.close }
pw.last.sync = true
pi = [ pw.last, pr.first, pe.first ]
begin
block.call(cid, *pi) if block_given?
ensure
pi.each { |fd| fd.close unless fd.closed? }
end
begin
e = Marshal.load ps.first
raise Exception === e ? e : "unknown failure!"
rescue EOFError
# Calling +fun+ did not raise exception.
ensure
ps.first.close
exit_status = Process.waitpid2(cid).last
end
exit_status
end
end
if $0 == __FILE__
require 'minitest/autorun'
class PForkTest < MiniTest::Unit::TestCase
include PFork
def test_fun_successful_return
fun = lambda { 'lucky me' }
status = pfork fun
assert_equal 0, status.exitstatus
end
def test_fun_raise_exception
msg = 'oh noes'
fun = lambda { raise msg }
err = assert_raises(RuntimeError) { pfork fun }
assert_equal msg, err.to_s
end
def test_fun_force_exit
exit_code = 43
fun = lambda { exit! exit_code }
status = pfork fun
assert_equal exit_code, status.exitstatus
end
def test_fun_normal_exit
exit_code = 43
fun = lambda { exit exit_code }
status = pfork fun
assert_equal exit_code, status.exitstatus
end
def test_io
via_msg = 'foo'
err_msg = 'bar'
fun = lambda do
$stdout.write $stdin.read
$stderr.write err_msg
end
out_actual, err_actual = nil, nil
status = pfork fun do |_, stdin, stdout, stderr|
stdin.write via_msg
stdin.close
out_actual = stdout.read
err_actual = stderr.read
end
assert_equal via_msg, out_actual
assert_equal err_msg, err_actual
assert_equal 0, status.exitstatus
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment