m2r 2.0.2 → 2.1.0.pre

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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.travis.yml +11 -3
  4. data/Vagrantfile +16 -0
  5. data/example/http_0mq.rb +4 -0
  6. data/kitchen/Rakefile +28 -0
  7. data/kitchen/auth.cfg +2 -0
  8. data/kitchen/cookbooks/build-essential/README.md +24 -0
  9. data/kitchen/cookbooks/build-essential/metadata.json +35 -0
  10. data/kitchen/cookbooks/build-essential/metadata.rb +10 -0
  11. data/kitchen/cookbooks/build-essential/recipes/default.rb +45 -0
  12. data/kitchen/cookbooks/essential/CHANGELOG.md +12 -0
  13. data/kitchen/cookbooks/essential/README.md +12 -0
  14. data/kitchen/cookbooks/essential/metadata.json +29 -0
  15. data/kitchen/cookbooks/essential/metadata.rb +6 -0
  16. data/kitchen/cookbooks/essential/recipes/default.rb +16 -0
  17. data/kitchen/cookbooks/m2r/CHANGELOG.md +12 -0
  18. data/kitchen/cookbooks/m2r/README.md +12 -0
  19. data/kitchen/cookbooks/m2r/metadata.json +29 -0
  20. data/kitchen/cookbooks/m2r/metadata.rb +6 -0
  21. data/kitchen/cookbooks/m2r/recipes/default.rb +13 -0
  22. data/kitchen/cookbooks/mongrel2/CHANGELOG.md +12 -0
  23. data/kitchen/cookbooks/mongrel2/README.md +12 -0
  24. data/kitchen/cookbooks/mongrel2/metadata.json +29 -0
  25. data/kitchen/cookbooks/mongrel2/metadata.rb +6 -0
  26. data/kitchen/cookbooks/mongrel2/recipes/default.rb +38 -0
  27. data/kitchen/cookbooks/ruby-build/README.md +12 -0
  28. data/kitchen/cookbooks/ruby-build/definitions/ruby.rb +65 -0
  29. data/kitchen/cookbooks/ruby-build/metadata.json +29 -0
  30. data/kitchen/cookbooks/ruby-build/metadata.rb +6 -0
  31. data/kitchen/cookbooks/ruby-build/recipes/default.rb +9 -0
  32. data/kitchen/cookbooks/zmq/CHANGELOG.md +12 -0
  33. data/kitchen/cookbooks/zmq/README.md +12 -0
  34. data/kitchen/cookbooks/zmq/metadata.json +29 -0
  35. data/kitchen/cookbooks/zmq/metadata.rb +6 -0
  36. data/kitchen/cookbooks/zmq/recipes/default.rb +36 -0
  37. data/kitchen/data_bags/README +1 -0
  38. data/kitchen/data_bags/vagrant.key +27 -0
  39. data/kitchen/data_bags/vagrant.pub +1 -0
  40. data/kitchen/m2r.cfg +5 -0
  41. data/kitchen/nodes/m2r.local.json +16 -0
  42. data/kitchen/roles/.gitkeep +0 -0
  43. data/kitchen/site-cookbooks/README +1 -0
  44. data/lib/m2r.rb +2 -0
  45. data/lib/m2r/connection.rb +30 -5
  46. data/lib/m2r/handler.rb +8 -0
  47. data/lib/m2r/multithread_handler.rb +27 -0
  48. data/lib/m2r/parser.rb +44 -0
  49. data/lib/m2r/rack_handler.rb +0 -2
  50. data/lib/m2r/request.rb +2 -21
  51. data/lib/m2r/version.rb +1 -1
  52. data/lib/rack/handler/mongrel2.rb +7 -3
  53. data/m2r.gemspec +3 -3
  54. data/test/support/test_handler.rb +29 -12
  55. data/test/test_helper.rb +1 -1
  56. data/test/unit/connection_test.rb +51 -16
  57. data/test/unit/handler_test.rb +33 -5
  58. data/test/unit/m2r_test.rb +2 -0
  59. data/test/unit/multithread_handler_test.rb +75 -0
  60. data/test/unit/rack_handler_test.rb +6 -2
  61. data/test/unit/{request_test.rb → request_parsing_test.rb} +5 -5
  62. metadata +71 -62
@@ -0,0 +1,44 @@
1
+ require 'm2r/request'
2
+
3
+ module M2R
4
+ # Mongrel2 Request Parser
5
+ # @api public
6
+ class Parser
7
+ # Parse Mongrel2 request received via ZMQ message
8
+ #
9
+ # @param [String] msg Monrel2 Request message formatted according to rules
10
+ # of creating it described it m2 manual.
11
+ # @return [Request]
12
+ #
13
+ # @api public
14
+ # @threadsafe true
15
+ def parse(msg)
16
+ sender, conn_id, path, rest = msg.split(' ', 4)
17
+
18
+ headers, rest = TNetstring.parse(rest)
19
+ body, _ = TNetstring.parse(rest)
20
+ headers = MultiJson.load(headers)
21
+ headers, mong = split_headers(headers)
22
+ headers = Headers.new headers, true
23
+ mong = Headers.new mong, true
24
+ Request.new(sender, conn_id, path, headers, mong, body)
25
+ end
26
+
27
+ private
28
+
29
+ def split_headers(headers)
30
+ http = {}
31
+ mongrel = {}
32
+ headers.each do |header, value|
33
+ if Request::MONGREL2_HEADERS.include?(header)
34
+ mongrel[header.downcase] = value
35
+ else
36
+ http[header] = value
37
+ end
38
+ end
39
+ return http, mongrel
40
+ end
41
+
42
+ end
43
+ end
44
+
@@ -10,8 +10,6 @@ module M2R
10
10
  def initialize(app, connection_factory, parser)
11
11
  @app = app
12
12
  super(connection_factory, parser)
13
-
14
- trap('INT') { stop }
15
13
  end
16
14
 
17
15
  def process(request)
@@ -57,16 +57,9 @@ module M2R
57
57
  # @return [Request]
58
58
  #
59
59
  # @api public
60
+ # @deprecated
60
61
  def self.parse(msg)
61
- sender, conn_id, path, rest = msg.split(' ', 4)
62
-
63
- headers, rest = TNetstring.parse(rest)
64
- body, _ = TNetstring.parse(rest)
65
- headers = MultiJson.load(headers)
66
- headers, mong = split_headers(headers)
67
- headers = Headers.new headers, true
68
- mong = Headers.new mong, true
69
- self.new(sender, conn_id, path, headers, mong, body)
62
+ Parser.new.parse(msg)
70
63
  end
71
64
 
72
65
  # @return [M2R::Headers] HTTP headers
@@ -120,17 +113,5 @@ module M2R
120
113
  method == 'JSON'
121
114
  end
122
115
 
123
- def self.split_headers(headers)
124
- http = {}
125
- mongrel = {}
126
- headers.each do |header, value|
127
- if MONGREL2_HEADERS.include?(header)
128
- mongrel[header.downcase] = value
129
- else
130
- http[header] = value
131
- end
132
- end
133
- return http, mongrel
134
- end
135
116
  end
136
117
  end
@@ -1,5 +1,5 @@
1
1
  module M2R
2
2
  # m2r gem version
3
3
  # @api public
4
- VERSION = '2.0.2'
4
+ VERSION = '2.1.0.pre'
5
5
  end
@@ -13,10 +13,14 @@ module Rack
13
13
  }
14
14
 
15
15
  def self.run(app, options = {})
16
- options = OpenStruct.new( DEFAULT_OPTIONS.merge(options) )
17
- parser = M2R::Request
18
- adapter = M2R::RackHandler.new(app, connection_factory(options), parser)
16
+ options = OpenStruct.new( DEFAULT_OPTIONS.merge(options) )
17
+ threadsafe_parser = M2R::Parser.new
18
+ adapter = M2R::RackHandler.new(app, connection_factory(options), threadsafe_parser)
19
+ graceful = Proc.new { adapter.stop }
20
+ trap("INT", &graceful)
21
+ trap("TERM", &graceful)
19
22
  adapter.listen
23
+ M2R.zmq_context.terminate
20
24
  end
21
25
 
22
26
  def self.valid_options
@@ -18,15 +18,15 @@ Gem::Specification.new do |gem|
18
18
  gem.require_paths = ["lib"]
19
19
  gem.version = M2R::VERSION
20
20
 
21
- gem.add_dependency "ffi-rzmq", "~> 0.9.3"
22
- gem.add_dependency "ffi", ">= 1.0.0"
21
+ gem.add_dependency "ffi-rzmq", ">= 1.0.1"
22
+ gem.add_dependency "ffi", ">= 1.0.0"
23
23
  gem.add_dependency "multi_json"
24
24
  gem.add_dependency "tnetstring"
25
25
 
26
26
  gem.add_development_dependency "rack"
27
27
  gem.add_development_dependency "rake"
28
28
  gem.add_development_dependency "minitest", "= 3.2.0"
29
- gem.add_development_dependency "mocha", "~> 0.12.1"
29
+ gem.add_development_dependency "mocha", ">= 0.14.0"
30
30
  gem.add_development_dependency "bbq", "= 0.0.4"
31
31
  gem.add_development_dependency "capybara-mechanize", "= 0.3.0"
32
32
  gem.add_development_dependency "activesupport", "~> 3.2.7"
@@ -1,55 +1,72 @@
1
+ require 'thread'
1
2
  require 'm2r/handler'
2
3
 
3
4
  class TestHandler < M2R::Handler
4
5
  attr_reader :called_methods
5
- def initialize(connection, parser)
6
+ def initialize(connection_factory, parser)
6
7
  super
8
+ @mutex = Mutex.new
7
9
  @called_methods = []
10
+ Thread.current[:called_methods] = []
8
11
  end
9
12
 
10
13
  def on_wait()
11
- unless @called_methods.empty?
14
+ unless Thread.current[:called_methods].empty?
12
15
  stop
13
16
  return
14
17
  end
15
- @called_methods << :wait
18
+ called_method :wait
16
19
  end
17
20
 
18
21
  def on_request(request)
19
- @called_methods << :request
22
+ called_method :request
20
23
  end
21
24
 
22
25
  def process(request)
23
- @called_methods << :process
26
+ called_method :process
24
27
  return "response"
25
28
  end
26
29
 
27
30
  def on_disconnect(request)
28
- @called_methods << :disconnect
31
+ called_method :disconnect
29
32
  end
30
33
 
31
34
  def on_upload_start(request)
32
- @called_methods << :start
35
+ called_method :start
33
36
  end
34
37
 
35
38
  def on_upload_done(request)
36
- @called_methods << :done
39
+ called_method :done
37
40
  end
38
41
 
39
42
  def after_process(request, response)
40
- @called_methods << :after
43
+ called_method :after
41
44
  return response
42
45
  end
43
46
 
44
47
  def after_reply(request, response)
45
- @called_methods << :reply
48
+ called_method :reply
46
49
  end
47
50
 
48
51
  def after_all(request, response)
49
- @called_methods << :all
52
+ called_method :all
50
53
  end
51
54
 
52
55
  def on_error(request, response, error)
53
- @called_methods << :error
56
+ called_method :error
54
57
  end
58
+
59
+ def on_interrupted
60
+ called_method :interrupted
61
+ end
62
+
63
+ private
64
+
65
+ def called_method(mth)
66
+ Thread.current[:called_methods] << mth
67
+ @mutex.synchronize do
68
+ @called_methods << mth
69
+ end
70
+ end
71
+
55
72
  end
@@ -1,5 +1,5 @@
1
1
  require 'minitest/autorun'
2
- require 'mocha'
2
+ require 'mocha/setup'
3
3
  require 'm2r'
4
4
 
5
5
  Dir[File.expand_path(File.join(__FILE__, '../support/*.rb'))].each { |m| require m }
@@ -11,17 +11,16 @@ module M2R
11
11
  @push = M2R.zmq_context.socket(ZMQ::PUSH)
12
12
  assert_equal 0, @push.bind(@request_addr), "Could not bind push socket in tests"
13
13
 
14
- @sub = M2R.zmq_context.socket(ZMQ::SUB)
15
- assert_equal 0, @sub.bind(@response_addr), "Could not bind sub socket in tests"
16
- @sub.setsockopt(ZMQ::SUBSCRIBE, "")
17
-
18
-
19
14
  @request_socket = M2R.zmq_context.socket(ZMQ::PULL)
20
15
  @request_socket.connect(@request_addr)
21
16
 
22
17
  @response_socket = M2R.zmq_context.socket(ZMQ::PUB)
23
- @response_socket.connect(@response_addr)
18
+ @response_socket.bind(@response_addr)
24
19
  @response_socket.setsockopt(ZMQ::IDENTITY, @sender_id = SecureRandom.uuid)
20
+
21
+ @sub = M2R.zmq_context.socket(ZMQ::SUB)
22
+ assert_equal 0, @sub.connect(@response_addr), "Could not connect sub socket in tests"
23
+ @sub.setsockopt(ZMQ::SUBSCRIBE, "")
25
24
  end
26
25
 
27
26
  def teardown
@@ -33,48 +32,48 @@ module M2R
33
32
 
34
33
  def test_receive_message
35
34
  connection = Connection.new(@request_socket, @response_socket)
36
- @push.send_string(msg = "1c5fd481-1121-49d8-a706-69127975db1a ebb407b2-49aa-48a5-9f96-9db121051484 / 2:{},0:,", ZMQ::NOBLOCK)
35
+ @push.send_string(msg = "1c5fd481-1121-49d8-a706-69127975db1a ebb407b2-49aa-48a5-9f96-9db121051484 / 2:{},0:,", ZMQ::NonBlocking)
37
36
  assert_equal msg, connection.receive
38
37
  end
39
38
 
40
39
  def test_deliver_message
41
40
  connection = Connection.new(@request_socket, @response_socket)
42
41
  connection.deliver('uuid', ['conn1', 'conn2'], 'ddaattaa')
43
- assert_equal 0, @sub.recv_string(msg = "")
42
+ assert @sub.recv_string(msg = "") > 0
44
43
  assert_equal "uuid 11:conn1 conn2, ddaattaa", msg
45
44
  end
46
45
 
47
46
  def test_string_reply_non_close
48
47
  connection = Connection.new(@request_socket, @response_socket)
49
48
  connection.reply( stub(sender: 'uuid', conn_id: 'conn1', close?: false), 'ddaattaa')
50
- assert_equal 0, @sub.recv_string(msg = "")
49
+ assert @sub.recv_string(msg = "") > 0
51
50
  assert_equal "uuid 5:conn1, ddaattaa", msg
52
- assert_equal -1, @sub.recv_string(msg = "", ZMQ::NOBLOCK)
51
+ assert_equal -1, @sub.recv_string(msg = "", ZMQ::NonBlocking)
53
52
  end
54
53
 
55
54
  def test_string_reply_close
56
55
  connection = Connection.new(@request_socket, @response_socket)
57
56
  connection.reply( stub(sender: 'uuid', conn_id: 'conn1', close?: true), 'ddaattaa')
58
- assert_equal 0, @sub.recv_string(msg = "")
57
+ assert @sub.recv_string(msg = "") > 0
59
58
  assert_equal "uuid 5:conn1, ddaattaa", msg
60
- assert_equal 0, @sub.recv_string(msg = "")
59
+ assert @sub.recv_string(msg = "") > 0
61
60
  assert_equal "uuid 5:conn1, ", msg
62
61
  end
63
62
 
64
63
  def test_response_reply_non_close
65
64
  connection = Connection.new(@request_socket, @response_socket)
66
65
  connection.reply( stub(sender: 'uuid', conn_id: 'conn1'), mock(to_s: 'ddaattaa', close?: false))
67
- assert_equal 0, @sub.recv_string(msg = "")
66
+ assert @sub.recv_string(msg = "") > 0
68
67
  assert_equal "uuid 5:conn1, ddaattaa", msg
69
- assert_equal -1, @sub.recv_string(msg = "", ZMQ::NOBLOCK)
68
+ assert_equal -1, @sub.recv_string(msg = "", ZMQ::NonBlocking)
70
69
  end
71
70
 
72
71
  def test_response_reply_close
73
72
  connection = Connection.new(@request_socket, @response_socket)
74
73
  connection.reply( stub(sender: 'uuid', conn_id: 'conn1'), mock(to_s: 'ddaattaa', close?: true))
75
- assert_equal 0, @sub.recv_string(msg = "")
74
+ assert @sub.recv_string(msg = "") > 0
76
75
  assert_equal "uuid 5:conn1, ddaattaa", msg
77
- assert_equal 0, @sub.recv_string(msg = "")
76
+ assert @sub.recv_string(msg = "") > 0
78
77
  assert_equal "uuid 5:conn1, ", msg
79
78
  end
80
79
 
@@ -84,6 +83,42 @@ module M2R
84
83
  assert_raises(Connection::Error) { connection.receive }
85
84
  end
86
85
 
86
+ def test_exception_erron_when_receiving
87
+ request_socket = mock(:recv_string => -1)
88
+ ZMQ::Util.expects(:errno).at_least_once.returns(4)
89
+ connection = Connection.new request_socket, nil
90
+ begin
91
+ connection.receive
92
+ flunk "exception expected"
93
+ rescue => er
94
+ assert_equal 4, er.errno
95
+ assert er.signal?
96
+ end
97
+ end
98
+
99
+ def test_exception_when_deliverying
100
+ ZMQ::Util.expects(:errno).at_least_once.returns(1)
101
+ response_socket = mock(:send_string => -1)
102
+ connection = Connection.new nil, response_socket
103
+ assert_raises(Connection::Error) { connection.deliver('uuid', ['connection_ids'], 'data') }
104
+ end
105
+
106
+ def test_exception_signal_retry
107
+ ZMQ::Util.expects(:errno).at_least_once.returns(4)
108
+ response_socket = mock
109
+ response_socket.expects(:send_string).times(3).returns(-1)
110
+ connection = Connection.new nil, response_socket
111
+ assert_raises(Connection::Error) { connection.deliver('uuid', ['connection_ids'], 'data') }
112
+ end
113
+
114
+ def test_exception_signal_retry_ok
115
+ ZMQ::Util.expects(:errno).at_least_once.returns(4)
116
+ response_socket = mock
117
+ response_socket.expects(:send_string).twice.returns(-1).then.returns(0)
118
+ connection = Connection.new nil, response_socket
119
+ assert connection.deliver('uuid', ['connection_ids'], 'data').size > 0
120
+ end
121
+
87
122
  def test_exception_when_deliverying
88
123
  response_socket = mock(:send_string => -1)
89
124
  connection = Connection.new nil, response_socket
@@ -4,7 +4,7 @@ module M2R
4
4
  class HandlerTest < MiniTest::Unit::TestCase
5
5
 
6
6
  def test_lifecycle_for_disconnect
7
- connection = stub(:receive => "")
7
+ connection = stub(:receive => "", :close => nil)
8
8
  connection.stubs(:connection).returns(connection)
9
9
  parser = stub(:parse => disconnect_request)
10
10
  h = TestHandler.new(connection, parser)
@@ -13,7 +13,7 @@ module M2R
13
13
  end
14
14
 
15
15
  def test_lifecycle_for_upload_start
16
- connection = stub(:receive => "")
16
+ connection = stub(:receive => "", :close => nil)
17
17
  connection.stubs(:connection).returns(connection)
18
18
  parser = stub(:parse => upload_start_request)
19
19
  h = TestHandler.new(connection, parser)
@@ -22,7 +22,7 @@ module M2R
22
22
  end
23
23
 
24
24
  def test_lifecycle_for_upload_done
25
- connection = stub(:receive => "", :reply => nil)
25
+ connection = stub(:receive => "", :reply => nil, :close => nil)
26
26
  connection.stubs(:connection).returns(connection)
27
27
  parser = stub(:parse => upload_done_request)
28
28
  h = TestHandler.new(connection, parser)
@@ -31,7 +31,7 @@ module M2R
31
31
  end
32
32
 
33
33
  def test_lifecycle_for_exception_when_getting_request
34
- connection = stub()
34
+ connection = stub(:close => nil)
35
35
  connection.stubs(:receive).raises(StandardError)
36
36
  connection.stubs(:connection).returns(connection)
37
37
  h = TestHandler.new(connection, nil)
@@ -40,7 +40,7 @@ module M2R
40
40
  end
41
41
 
42
42
  def test_lifecycle_for_exception_when_processing
43
- connection = stub(:receive => "", :reply => nil)
43
+ connection = stub(:receive => "", :reply => nil, :close => nil)
44
44
  connection.stubs(:connection).returns(connection)
45
45
  parser = stub(:parse => request)
46
46
  h = TestHandler.new(connection, parser)
@@ -54,6 +54,34 @@ module M2R
54
54
  assert_equal [:wait, :request, :process, :all, :error], h.called_methods
55
55
  end
56
56
 
57
+ def test_signal_when_receive
58
+ e = Connection::Error.new.tap{|x| x.errno = 4}
59
+ connection = stub(:reply => nil, :close => nil)
60
+ connection.stubs(:connection).returns(connection)
61
+ connection.expects(:receive).raises(e).then.returns("").twice
62
+ parser = stub(:parse => request)
63
+ h = TestHandler.new(connection, parser)
64
+ h.extend(Module.new(){
65
+ def on_wait
66
+ if @called_methods.size > 2
67
+ stop
68
+ return
69
+ end
70
+ @called_methods << :wait
71
+ end
72
+ })
73
+ h.listen
74
+ assert_equal [:wait, :interrupted, :wait, :request, :process, :after, :reply, :all], h.called_methods
75
+ end
76
+
77
+ def test_connection_closed
78
+ connection = mock(:close => nil)
79
+ connection.expects(:connection).returns(connection)
80
+ h = TestHandler.new(connection, nil)
81
+ h.stop
82
+ h.listen
83
+ end
84
+
57
85
 
58
86
  private
59
87
 
@@ -34,6 +34,8 @@ module M2R
34
34
  Timeout.timeout(5) do
35
35
  threads.each(&:join)
36
36
  end
37
+
38
+ M2R.zmq_context = nil
37
39
  end
38
40
 
39
41
  end
@@ -0,0 +1,75 @@
1
+ require 'test_helper'
2
+
3
+ module M2R
4
+ class MultithreadHandlerTest < MiniTest::Unit::TestCase
5
+ SLEEP_TIME = 3
6
+ MARGIN_TIME = 1
7
+ WAIT_TIME = 1
8
+
9
+ class ThreadTestHandler < TestHandler
10
+ def process(request)
11
+ sleep(SLEEP_TIME)
12
+ super
13
+ return Thread.current.object_id.to_s
14
+ end
15
+ end
16
+
17
+ class TestSinglethreadHandlerFactory
18
+ def initialize(connection_factory, parser)
19
+ @connection_factory = connection_factory
20
+ @parser = parser
21
+ end
22
+
23
+ def new
24
+ ThreadTestHandler.new(@connection_factory, @parser)
25
+ end
26
+ end
27
+
28
+ def setup
29
+ @request_addr = "inproc://#{SecureRandom.hex}"
30
+ @response_addr = "inproc://#{SecureRandom.hex}"
31
+
32
+ @push = M2R.zmq_context.socket(ZMQ::PUSH)
33
+ assert_equal 0, @push.bind(@request_addr), "Could not bind push socket in tests"
34
+
35
+ @sub = M2R.zmq_context.socket(ZMQ::SUB)
36
+ assert_equal 0, @sub.bind(@response_addr), "Could not bind sub socket in tests"
37
+ @sub.setsockopt(ZMQ::SUBSCRIBE, "")
38
+ end
39
+
40
+ def teardown
41
+ @push.close if @push
42
+ @sub.close if @sub
43
+ end
44
+
45
+ def test_threads_are_processing_request_simultaneously
46
+ cf = ConnectionFactory.new(ConnectionFactory::Options.new(nil, @request_addr, @response_addr))
47
+ par = Parser.new
48
+ mth = MultithreadHandler.new(TestSinglethreadHandlerFactory.new(cf, par))
49
+ mth.listen
50
+ sleep(WAIT_TIME)
51
+
52
+ start = Time.now
53
+ 8.times do |i|
54
+ @push.send_string(msg = "1c5fd481-1121-49d8-a706-69127975db1a ebb407b2-49aa-48a5-9f96-9db12105148#{i} / 2:{},1:#{i},", ZMQ::NonBlocking)
55
+ end
56
+ responses = 16.times.map do
57
+ @sub.recv_string(msg = "")
58
+ msg
59
+ end
60
+ finish = Time.now
61
+
62
+ mth.threads.each do |t|
63
+ t.join
64
+ end
65
+
66
+ blob = responses.join("\n")
67
+ mth.threads.each do |t|
68
+ assert blob.include?(", #{t.object_id}")
69
+ end
70
+
71
+ delta = finish - start
72
+ assert_in_delta(SLEEP_TIME, delta, MARGIN_TIME)
73
+ end
74
+ end
75
+ end