Created

Embed URL

HTTPS clone URL

SSH clone URL

You can clone with HTTPS or SSH.

Download Gist

Test global state changing code with fork

View fork_test_demo.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
# 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
View fork_test_demo.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
# 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
Something went wrong with that request. Please try again.