rack-rabbit 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/EXAMPLES.md +212 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +21 -0
- data/README.md +412 -0
- data/Rakefile +5 -0
- data/bin/rack-rabbit +96 -0
- data/bin/rr +99 -0
- data/lib/rack-rabbit.rb +63 -0
- data/lib/rack-rabbit/adapter.rb +85 -0
- data/lib/rack-rabbit/adapter/amqp.rb +114 -0
- data/lib/rack-rabbit/adapter/bunny.rb +87 -0
- data/lib/rack-rabbit/adapter/mock.rb +92 -0
- data/lib/rack-rabbit/client.rb +181 -0
- data/lib/rack-rabbit/config.rb +260 -0
- data/lib/rack-rabbit/handler.rb +44 -0
- data/lib/rack-rabbit/message.rb +95 -0
- data/lib/rack-rabbit/middleware/program_name.rb +34 -0
- data/lib/rack-rabbit/response.rb +43 -0
- data/lib/rack-rabbit/server.rb +263 -0
- data/lib/rack-rabbit/signals.rb +62 -0
- data/lib/rack-rabbit/subscriber.rb +77 -0
- data/lib/rack-rabbit/worker.rb +84 -0
- data/rack-rabbit.gemspec +26 -0
- data/test/apps/config.ru +7 -0
- data/test/apps/custom.conf +27 -0
- data/test/apps/custom.ru +7 -0
- data/test/apps/empty.conf +1 -0
- data/test/apps/error.ru +7 -0
- data/test/apps/mirror.ru +19 -0
- data/test/apps/sinatra.ru +37 -0
- data/test/apps/sleep.ru +21 -0
- data/test/test_case.rb +154 -0
- data/test/unit/middleware/test_program_name.rb +32 -0
- data/test/unit/test_client.rb +275 -0
- data/test/unit/test_config.rb +403 -0
- data/test/unit/test_handler.rb +92 -0
- data/test/unit/test_message.rb +213 -0
- data/test/unit/test_response.rb +59 -0
- data/test/unit/test_signals.rb +45 -0
- data/test/unit/test_subscriber.rb +140 -0
- metadata +91 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
module RackRabbit
|
2
|
+
class Signals
|
3
|
+
|
4
|
+
# The RackRabbit server process has a single primary thread, but it doesn't
|
5
|
+
# actually need to do any work once it has spun up the worker processes. I
|
6
|
+
# need it to hibernate until one (or more) signal event's occurs.
|
7
|
+
#
|
8
|
+
# Originally I tried to use the standard Thread::Queue, it uses a Mutex to
|
9
|
+
# perform a blocking pop on the Queue. However Ruby (2.x) won't let the last
|
10
|
+
# active thread hibernate (it's overly aggressive at deadlock prevention)
|
11
|
+
#
|
12
|
+
# So, instead of using a Mutex based Queue, I can use a blocking select
|
13
|
+
# on an IO pipe, then when the signal handler pushes into the Queue
|
14
|
+
# it can also write to the pipe in order to "awaken" the primary thread.
|
15
|
+
#
|
16
|
+
# FYI: this is the same underlying idea that is used by the Unicorn master
|
17
|
+
# process, I've just encapsulated it in a Signals class
|
18
|
+
#
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@reader, @writer = IO.pipe
|
22
|
+
@queue = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def close
|
26
|
+
@reader.close
|
27
|
+
@writer.close
|
28
|
+
@reader = @writer = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def closed?
|
32
|
+
@reader.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
def push(item)
|
36
|
+
raise RuntimeError, "closed" if closed?
|
37
|
+
@queue << item
|
38
|
+
awaken
|
39
|
+
end
|
40
|
+
|
41
|
+
def pop(options = {})
|
42
|
+
raise RuntimeError, "closed" if closed?
|
43
|
+
if @queue.empty? && (:timeout == hibernate(options[:timeout]))
|
44
|
+
:timeout
|
45
|
+
else
|
46
|
+
@queue.shift
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def awaken
|
53
|
+
@writer.write '.'
|
54
|
+
end
|
55
|
+
|
56
|
+
def hibernate(seconds = nil)
|
57
|
+
return :timeout unless IO.select([@reader], nil, nil, seconds)
|
58
|
+
@reader.readchar
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'rack-rabbit/adapter'
|
2
|
+
require 'rack-rabbit/handler'
|
3
|
+
|
4
|
+
module RackRabbit
|
5
|
+
class Subscriber
|
6
|
+
|
7
|
+
#--------------------------------------------------------------------------
|
8
|
+
|
9
|
+
attr_reader :config, # configuration options provided by the Server
|
10
|
+
:logger, # convenience for config.logger
|
11
|
+
:rabbit, # rabbitMQ adapter constructed by this class
|
12
|
+
:handler, # actually does the work of handling the RACK request/response
|
13
|
+
:lock # mutex to synchronize with Worker#shutdown for graceful QUIT handling
|
14
|
+
|
15
|
+
#--------------------------------------------------------------------------
|
16
|
+
|
17
|
+
def initialize(rabbit, handler, lock, config)
|
18
|
+
@rabbit = rabbit
|
19
|
+
@handler = handler
|
20
|
+
@lock = lock
|
21
|
+
@config = config
|
22
|
+
@logger = config.logger
|
23
|
+
end
|
24
|
+
|
25
|
+
#--------------------------------------------------------------------------
|
26
|
+
|
27
|
+
def subscribe
|
28
|
+
rabbit.startup
|
29
|
+
rabbit.connect
|
30
|
+
rabbit.subscribe(:queue => config.queue,
|
31
|
+
:exchange => config.exchange,
|
32
|
+
:exchange_type => config.exchange_type,
|
33
|
+
:routing_key => config.routing_key,
|
34
|
+
:ack => config.ack) do |message|
|
35
|
+
lock.synchronize do
|
36
|
+
start = Time.now
|
37
|
+
response = handle(message)
|
38
|
+
finish = Time.now
|
39
|
+
logger.info "\"#{message.method} #{message.path}\" [#{response.status}] - #{"%.4f" % (finish - start)}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def unsubscribe
|
45
|
+
rabbit.disconnect
|
46
|
+
rabbit.shutdown
|
47
|
+
end
|
48
|
+
|
49
|
+
#==========================================================================
|
50
|
+
# PRIVATE IMPLEMENTATION
|
51
|
+
#==========================================================================
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def handle(message)
|
56
|
+
|
57
|
+
response = handler.handle(message) # does all the Rack related work
|
58
|
+
|
59
|
+
if message.should_reply?
|
60
|
+
rabbit.publish(response.body, message.get_reply_properties(response, config))
|
61
|
+
end
|
62
|
+
|
63
|
+
if config.ack && !message.acknowledged? && !message.rejected?
|
64
|
+
if response.succeeded?
|
65
|
+
message.ack
|
66
|
+
else
|
67
|
+
message.reject
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
response
|
72
|
+
end
|
73
|
+
|
74
|
+
#--------------------------------------------------------------------------
|
75
|
+
|
76
|
+
end # class Subscriber
|
77
|
+
end # module RackRabbit
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'rack-rabbit/signals'
|
2
|
+
require 'rack-rabbit/subscriber'
|
3
|
+
require 'rack-rabbit/handler'
|
4
|
+
require 'rack-rabbit/adapter'
|
5
|
+
|
6
|
+
module RackRabbit
|
7
|
+
class Worker
|
8
|
+
|
9
|
+
#--------------------------------------------------------------------------
|
10
|
+
|
11
|
+
attr_reader :config, # provided by the Server
|
12
|
+
:logger, # convenience for config.logger
|
13
|
+
:signals, # blocking Q for signal handling
|
14
|
+
:lock, # mutex to synchronise with Subscriber#subscribe for graceful QUIT handling
|
15
|
+
:rabbit, # interface to rabbit MQ
|
16
|
+
:subscriber, # actually does the work of subscribing to the rabbit queue
|
17
|
+
:handler # actually does the work of handling the rack request/response
|
18
|
+
|
19
|
+
#--------------------------------------------------------------------------
|
20
|
+
|
21
|
+
def initialize(config, app)
|
22
|
+
@config = config
|
23
|
+
@logger = config.logger
|
24
|
+
@signals = Signals.new
|
25
|
+
@lock = Mutex.new
|
26
|
+
@rabbit = Adapter.load(config.rabbit)
|
27
|
+
@handler = Handler.new(app, config)
|
28
|
+
@subscriber = Subscriber.new(rabbit, handler, lock, config)
|
29
|
+
end
|
30
|
+
|
31
|
+
#--------------------------------------------------------------------------
|
32
|
+
|
33
|
+
def run
|
34
|
+
|
35
|
+
logger.info "STARTED a new worker with PID #{Process.pid}"
|
36
|
+
|
37
|
+
trap_signals
|
38
|
+
|
39
|
+
subscriber.subscribe
|
40
|
+
|
41
|
+
while true
|
42
|
+
sig = signals.pop # BLOCKS until there is a signal
|
43
|
+
case sig
|
44
|
+
when :INT then shutdown(:INT)
|
45
|
+
when :QUIT then shutdown(:QUIT)
|
46
|
+
when :TERM then shutdown(:TERM)
|
47
|
+
else
|
48
|
+
raise RuntimeError, "unknown signal #{sig}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
ensure
|
53
|
+
subscriber.unsubscribe
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
#--------------------------------------------------------------------------
|
58
|
+
|
59
|
+
def shutdown(sig)
|
60
|
+
lock.lock if sig == :QUIT # graceful shutdown should wait for any pending message handler to finish
|
61
|
+
logger.info "#{RackRabbit.friendly_signal(sig)} worker #{Process.pid}"
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
|
65
|
+
#--------------------------------------------------------------------------
|
66
|
+
|
67
|
+
def trap_signals # overwrite the handlers inherited from the server process
|
68
|
+
|
69
|
+
[:QUIT, :TERM, :INT].each do |sig|
|
70
|
+
trap(sig) do
|
71
|
+
signals.push(sig)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
trap(:CHLD, :DEFAULT)
|
76
|
+
trap(:TTIN, nil)
|
77
|
+
trap(:TTOU, nil)
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
#--------------------------------------------------------------------------
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
data/rack-rabbit.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
LIB = File.expand_path("lib", File.dirname(__FILE__))
|
3
|
+
$LOAD_PATH.unshift LIB unless $LOAD_PATH.include?(LIB)
|
4
|
+
|
5
|
+
require 'rack-rabbit'
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
|
9
|
+
s.name = "rack-rabbit"
|
10
|
+
s.version = RackRabbit::VERSION
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.authors = ["Jake Gordon"]
|
13
|
+
s.email = ["jake@codeincomplete.com"]
|
14
|
+
s.homepage = "https://github.com/jakesgordon/rack-rabbit"
|
15
|
+
s.summary = RackRabbit::SUMMARY
|
16
|
+
|
17
|
+
s.has_rdoc = false
|
18
|
+
s.extra_rdoc_files = ["README.md"]
|
19
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
20
|
+
s.files = `git ls-files `.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
s.licenses = ["MIT"]
|
25
|
+
|
26
|
+
end
|
data/test/apps/config.ru
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
rack_file "custom.ru"
|
3
|
+
|
4
|
+
rabbit :host => "10.10.10.10",
|
5
|
+
:port => "1234",
|
6
|
+
:adapter => "amqp"
|
7
|
+
|
8
|
+
queue "myqueue"
|
9
|
+
exchange "myexchange"
|
10
|
+
exchange_type "topic"
|
11
|
+
routing_key "myroute"
|
12
|
+
app_id "myapp"
|
13
|
+
workers 7
|
14
|
+
min_workers 3
|
15
|
+
max_workers 42
|
16
|
+
ack true
|
17
|
+
preload_app true
|
18
|
+
daemonize true
|
19
|
+
logfile "myapp.log"
|
20
|
+
pidfile "myapp.pid"
|
21
|
+
log_level :fatal
|
22
|
+
|
23
|
+
class ::MyLogger < Logger
|
24
|
+
end
|
25
|
+
|
26
|
+
logger MyLogger.new($stderr)
|
27
|
+
|
data/test/apps/custom.ru
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# empty rack-rabbit configuration file
|
data/test/apps/error.ru
ADDED
data/test/apps/mirror.ru
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class MirrorApp
|
4
|
+
def self.call(env)
|
5
|
+
|
6
|
+
request = Rack::Request.new env
|
7
|
+
result = {
|
8
|
+
:method => request.request_method,
|
9
|
+
:path => request.path_info,
|
10
|
+
:params => request.params,
|
11
|
+
:body => request.body.read
|
12
|
+
}
|
13
|
+
|
14
|
+
[ 200, {}, [ result.to_json ] ]
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
run MirrorApp
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
class MyApp < Sinatra::Base
|
4
|
+
|
5
|
+
set :logging, nil # skip sinatra logging middleware, use the env['rack.logger'] provided by the rack-rabbit server
|
6
|
+
|
7
|
+
get "/hello" do
|
8
|
+
"Hello World"
|
9
|
+
end
|
10
|
+
|
11
|
+
post "/submit" do
|
12
|
+
"Submitted #{request.body.read}"
|
13
|
+
end
|
14
|
+
|
15
|
+
get "/error" do
|
16
|
+
raise "uh oh"
|
17
|
+
end
|
18
|
+
|
19
|
+
get "/sleep/:seconds" do
|
20
|
+
slumber params[:seconds].to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
post "/sleep/:seconds" do
|
24
|
+
slumber params[:seconds].to_i
|
25
|
+
end
|
26
|
+
|
27
|
+
def slumber(seconds)
|
28
|
+
seconds.times do |i|
|
29
|
+
logger.info "#{request.path_info} - #{i}"
|
30
|
+
sleep(1)
|
31
|
+
end
|
32
|
+
"Slept for #{seconds}"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
run MyApp
|
data/test/apps/sleep.ru
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class SleepApp
|
2
|
+
|
3
|
+
def self.call(env)
|
4
|
+
|
5
|
+
request = Rack::Request.new env
|
6
|
+
logger = request.logger
|
7
|
+
path = request.path_info.to_s
|
8
|
+
|
9
|
+
duration = path.split("/").last.to_i
|
10
|
+
duration.times do |n|
|
11
|
+
logger.info "#{path} - #{n}"
|
12
|
+
sleep(1)
|
13
|
+
end
|
14
|
+
|
15
|
+
[ 200, {}, [ "Slept for: #{duration}" ] ]
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
run SleepApp
|
data/test/test_case.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'mocha/mini_test'
|
3
|
+
require 'rack'
|
4
|
+
require 'rack/builder'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'timecop'
|
7
|
+
require 'pp'
|
8
|
+
|
9
|
+
require 'rack-rabbit' # top level module
|
10
|
+
|
11
|
+
require 'rack-rabbit/subscriber' # subscribes to rabbit queue/exchange and passes messages on to a handler
|
12
|
+
require 'rack-rabbit/handler' # converts rabbit messages to rack environments and calls a rack app to handle the request
|
13
|
+
require 'rack-rabbit/adapter' # abstract interface to rabbitMQ
|
14
|
+
require 'rack-rabbit/message' # a rabbitMQ message
|
15
|
+
require 'rack-rabbit/response' # a rack response
|
16
|
+
|
17
|
+
require 'rack-rabbit/client' # client code for making requests
|
18
|
+
require 'rack-rabbit/worker' # worker process
|
19
|
+
require 'rack-rabbit/server' # server process
|
20
|
+
require 'rack-rabbit/config' # server configuration
|
21
|
+
require 'rack-rabbit/signals' # process signal queue
|
22
|
+
|
23
|
+
module RackRabbit
|
24
|
+
class TestCase < Minitest::Unit::TestCase
|
25
|
+
|
26
|
+
#--------------------------------------------------------------------------
|
27
|
+
|
28
|
+
EMPTY_CONFIG = File.expand_path("apps/empty.conf", File.dirname(__FILE__))
|
29
|
+
CUSTOM_CONFIG = File.expand_path("apps/custom.conf", File.dirname(__FILE__))
|
30
|
+
|
31
|
+
#--------------------------------------------------------------------------
|
32
|
+
|
33
|
+
DEFAULT_RACK_APP = File.expand_path("apps/config.ru", File.dirname(__FILE__))
|
34
|
+
CUSTOM_RACK_APP = File.expand_path("apps/custom.ru", File.dirname(__FILE__))
|
35
|
+
ERROR_RACK_APP = File.expand_path("apps/error.ru", File.dirname(__FILE__))
|
36
|
+
MIRROR_RACK_APP = File.expand_path("apps/mirror.ru", File.dirname(__FILE__))
|
37
|
+
|
38
|
+
#--------------------------------------------------------------------------
|
39
|
+
|
40
|
+
APP_ID = "app.id"
|
41
|
+
DELIVERY_TAG = "delivery.tag"
|
42
|
+
REPLY_TO = "reply.queue"
|
43
|
+
CORRELATION_ID = "correlation.id"
|
44
|
+
QUEUE = "my.queue"
|
45
|
+
REPLY_QUEUE = "reply.queue"
|
46
|
+
EXCHANGE = "my.exchange"
|
47
|
+
ROUTE = "my.route"
|
48
|
+
BODY = "body"
|
49
|
+
PATH = "/foo/bar"
|
50
|
+
QUERY = "a=b&c=d"
|
51
|
+
URI = "#{PATH}?#{QUERY}"
|
52
|
+
PRIORITY = 7
|
53
|
+
|
54
|
+
module CONTENT
|
55
|
+
ASCII = "iso-8859-1"
|
56
|
+
UTF8 = "utf-8"
|
57
|
+
PLAIN_TEXT = "text/plain"
|
58
|
+
PLAIN_TEXT_UTF8 = "text/plain; charset=\"utf-8\""
|
59
|
+
FORM_URLENCODED = "application/x-www-form-urlencoded"
|
60
|
+
FORM_URLENCODED_UTF8 = "application/x-www-form-urlencoded; charset=\"utf-8\""
|
61
|
+
JSON = "application/json"
|
62
|
+
JSON_UTF8 = "application/json; charset=\"utf-8\""
|
63
|
+
JSON_ASCII = "application/json; charset=\"iso-8859-1\""
|
64
|
+
end
|
65
|
+
|
66
|
+
#--------------------------------------------------------------------------
|
67
|
+
|
68
|
+
NullLogger = Rack::NullLogger.new($stdout)
|
69
|
+
|
70
|
+
#--------------------------------------------------------------------------
|
71
|
+
|
72
|
+
def default_config
|
73
|
+
build_config( :rabbit => nil, :logger => nil ) # special case for select tests that want TRUE defaults (not the :mock adapter or NullLogger needed in 80% of other tests)
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_config(options = {})
|
77
|
+
Config.new({
|
78
|
+
:validate => false, # skip validation for most tests
|
79
|
+
:rack_file => DEFAULT_RACK_APP, # required - so default to sample app
|
80
|
+
:rabbit => { :adapter => :mock }, # use RackRabbit::Adapter::Mock to mock out rabbit MQ
|
81
|
+
:logger => NullLogger # suppress logging during tests
|
82
|
+
}.merge(options))
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_client(options = {})
|
86
|
+
Client.new({ :adapter => :mock }.merge(options))
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_message(options = {})
|
90
|
+
options[:headers] ||= {}
|
91
|
+
options[:headers][RackRabbit::HEADER::METHOD] ||= options.delete(:method) # convenience to make calling code a little more compact
|
92
|
+
options[:headers][RackRabbit::HEADER::PATH] ||= options.delete(:path) # (ditto)
|
93
|
+
options[:headers][RackRabbit::HEADER::STATUS] ||= options.delete(:status) # (ditto)
|
94
|
+
Message.new(options[:delivery_tag], OpenStruct.new(options), options[:body], options[:rabbit] || build_rabbit)
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_response(status, body, headers = {})
|
98
|
+
headers ||= {}
|
99
|
+
headers[RackRabbit::HEADER::CONTENT_TYPE] ||= headers.delete(:content_type) # convenience to make calling code a little more compact
|
100
|
+
headers[RackRabbit::HEADER::CONTENT_ENCODING] ||= headers.delete(:content_encoding) # (ditto)
|
101
|
+
Response.new(status, headers, body)
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_app(rack_file)
|
105
|
+
Rack::Builder.parse_file(rack_file)[0]
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_handler(options = {})
|
109
|
+
config = options[:config] || build_config(options)
|
110
|
+
app = options[:app] || build_app(config.rack_file)
|
111
|
+
Handler.new(app, config)
|
112
|
+
end
|
113
|
+
|
114
|
+
def build_subscriber(options = {})
|
115
|
+
rabbit = options[:rabbit] || build_rabbit(options)
|
116
|
+
config = options[:config] || build_config(options)
|
117
|
+
handler = options[:handler] || build_handler(options.merge(:config => config))
|
118
|
+
Subscriber.new(rabbit, handler, Mutex.new, handler.config)
|
119
|
+
end
|
120
|
+
|
121
|
+
def build_rabbit(options = {})
|
122
|
+
Adapter.load({ :adapter => :mock }.merge(options))
|
123
|
+
end
|
124
|
+
|
125
|
+
#--------------------------------------------------------------------------
|
126
|
+
|
127
|
+
def assert_raises_argument_error(message = nil, &block)
|
128
|
+
e = assert_raises(ArgumentError, &block)
|
129
|
+
assert_match(/#{message}/, e.message) unless message.nil?
|
130
|
+
end
|
131
|
+
|
132
|
+
#--------------------------------------------------------------------------
|
133
|
+
|
134
|
+
def measure
|
135
|
+
start = Time.now
|
136
|
+
yield
|
137
|
+
finish = Time.now
|
138
|
+
finish - start
|
139
|
+
end
|
140
|
+
|
141
|
+
#--------------------------------------------------------------------------
|
142
|
+
|
143
|
+
def with_program_name(name)
|
144
|
+
original = $PROGRAM_NAME
|
145
|
+
$PROGRAM_NAME = name
|
146
|
+
yield
|
147
|
+
ensure
|
148
|
+
$PROGRAM_NAME = original
|
149
|
+
end
|
150
|
+
|
151
|
+
#--------------------------------------------------------------------------
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|