rack-rabbit 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|