Created
September 21, 2011 14:05
-
-
Save tkareine/1232111 to your computer and use it in GitHub Desktop.
Test global state changing code with fork
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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