mysql2 0.3.18 → 0.4.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -0
  3. data/LICENSE +21 -0
  4. data/README.md +135 -55
  5. data/examples/eventmachine.rb +1 -1
  6. data/examples/threaded.rb +4 -6
  7. data/ext/mysql2/client.c +368 -197
  8. data/ext/mysql2/client.h +13 -3
  9. data/ext/mysql2/extconf.rb +118 -35
  10. data/ext/mysql2/infile.c +2 -2
  11. data/ext/mysql2/mysql2_ext.c +1 -0
  12. data/ext/mysql2/mysql2_ext.h +7 -6
  13. data/ext/mysql2/mysql_enc_name_to_ruby.h +2 -2
  14. data/ext/mysql2/mysql_enc_to_ruby.h +25 -22
  15. data/ext/mysql2/result.c +512 -138
  16. data/ext/mysql2/result.h +13 -6
  17. data/ext/mysql2/statement.c +585 -0
  18. data/ext/mysql2/statement.h +19 -0
  19. data/lib/mysql2/client.rb +85 -26
  20. data/lib/mysql2/console.rb +1 -1
  21. data/lib/mysql2/em.rb +5 -6
  22. data/lib/mysql2/error.rb +18 -27
  23. data/lib/mysql2/field.rb +3 -0
  24. data/lib/mysql2/statement.rb +17 -0
  25. data/lib/mysql2/version.rb +1 -1
  26. data/lib/mysql2.rb +38 -18
  27. data/spec/configuration.yml.example +0 -6
  28. data/spec/em/em_spec.rb +22 -21
  29. data/spec/mysql2/client_spec.rb +525 -388
  30. data/spec/mysql2/error_spec.rb +38 -39
  31. data/spec/mysql2/result_spec.rb +223 -214
  32. data/spec/mysql2/statement_spec.rb +757 -0
  33. data/spec/spec_helper.rb +80 -59
  34. data/spec/ssl/ca-cert.pem +17 -0
  35. data/spec/ssl/ca-key.pem +27 -0
  36. data/spec/ssl/ca.cnf +22 -0
  37. data/spec/ssl/cert.cnf +22 -0
  38. data/spec/ssl/client-cert.pem +17 -0
  39. data/spec/ssl/client-key.pem +27 -0
  40. data/spec/ssl/client-req.pem +15 -0
  41. data/spec/ssl/gen_certs.sh +48 -0
  42. data/spec/ssl/pkcs8-client-key.pem +28 -0
  43. data/spec/ssl/pkcs8-server-key.pem +28 -0
  44. data/spec/ssl/server-cert.pem +17 -0
  45. data/spec/ssl/server-key.pem +27 -0
  46. data/spec/ssl/server-req.pem +15 -0
  47. data/support/mysql_enc_to_ruby.rb +7 -8
  48. data/support/ruby_enc_to_mysql.rb +1 -1
  49. metadata +42 -47
data/lib/mysql2/client.rb CHANGED
@@ -1,24 +1,28 @@
1
1
  module Mysql2
2
2
  class Client
3
3
  attr_reader :query_options, :read_timeout
4
- @@default_query_options = {
5
- :as => :hash, # the type of object you want each row back as; also supports :array (an array of values)
6
- :async => false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result
7
- :cast_booleans => false, # cast tinyint(1) fields as true/false in ruby
8
- :symbolize_keys => false, # return field names as symbols instead of strings
9
- :database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in
10
- :application_timezone => nil, # timezone Mysql2 will convert to before handing the object back to the caller
11
- :cache_rows => true, # tells Mysql2 to use it's internal row cache for results
12
- :connect_flags => REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION,
13
- :cast => true,
14
- :default_file => nil,
15
- :default_group => nil
16
- }
4
+
5
+ def self.default_query_options
6
+ @default_query_options ||= {
7
+ :as => :hash, # the type of object you want each row back as; also supports :array (an array of values)
8
+ :async => false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result
9
+ :cast_booleans => false, # cast tinyint(1) fields as true/false in ruby
10
+ :symbolize_keys => false, # return field names as symbols instead of strings
11
+ :database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in
12
+ :application_timezone => nil, # timezone Mysql2 will convert to before handing the object back to the caller
13
+ :cache_rows => true, # tells Mysql2 to use its internal row cache for results
14
+ :connect_flags => REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION,
15
+ :cast => true,
16
+ :default_file => nil,
17
+ :default_group => nil,
18
+ }
19
+ end
17
20
 
18
21
  def initialize(opts = {})
19
- opts = Mysql2::Util.key_hash_as_symbols( opts )
22
+ fail Mysql2::Error, "Options parameter must be a Hash" unless opts.is_a? Hash
23
+ opts = Mysql2::Util.key_hash_as_symbols(opts)
20
24
  @read_timeout = nil
21
- @query_options = @@default_query_options.dup
25
+ @query_options = self.class.default_query_options.dup
22
26
  @query_options.merge! opts
23
27
 
24
28
  initialize_ext
@@ -26,13 +30,14 @@ module Mysql2
26
30
  # Set default connect_timeout to avoid unlimited retries from signal interruption
27
31
  opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout)
28
32
 
29
- [:reconnect, :connect_timeout, :local_infile, :read_timeout, :write_timeout, :default_file, :default_group, :secure_auth, :init_command].each do |key|
33
+ # TODO: stricter validation rather than silent massaging
34
+ [:reconnect, :connect_timeout, :local_infile, :read_timeout, :write_timeout, :default_file, :default_group, :secure_auth, :init_command, :automatic_close, :enable_cleartext_plugin].each do |key|
30
35
  next unless opts.key?(key)
31
36
  case key
32
- when :reconnect, :local_infile, :secure_auth
33
- send(:"#{key}=", !!opts[key])
37
+ when :reconnect, :local_infile, :secure_auth, :automatic_close, :enable_cleartext_plugin
38
+ send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
34
39
  when :connect_timeout, :read_timeout, :write_timeout
35
- send(:"#{key}=", opts[key].to_i)
40
+ send(:"#{key}=", Integer(opts[key])) unless opts[key].nil?
36
41
  else
37
42
  send(:"#{key}=", opts[key])
38
43
  end
@@ -43,10 +48,25 @@ module Mysql2
43
48
 
44
49
  ssl_options = opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher)
45
50
  ssl_set(*ssl_options) if ssl_options.any?
51
+ self.ssl_mode = parse_ssl_mode(opts[:ssl_mode]) if opts[:ssl_mode]
52
+
53
+ case opts[:flags]
54
+ when Array
55
+ flags = parse_flags_array(opts[:flags], @query_options[:connect_flags])
56
+ when String
57
+ flags = parse_flags_array(opts[:flags].split(' '), @query_options[:connect_flags])
58
+ when Integer
59
+ flags = @query_options[:connect_flags] | opts[:flags]
60
+ else
61
+ flags = @query_options[:connect_flags]
62
+ end
63
+
64
+ # SSL verify is a connection flag rather than a mysql_ssl_set option
65
+ flags |= SSL_VERIFY_SERVER_CERT if opts[:sslverify] && ssl_options.any?
46
66
 
47
- if [:user,:pass,:hostname,:dbname,:db,:sock].any?{|k| @query_options.has_key?(k) }
67
+ if [:user, :pass, :hostname, :dbname, :db, :sock].any? { |k| @query_options.key?(k) }
48
68
  warn "============= WARNING FROM mysql2 ============="
49
- warn "The options :user, :pass, :hostname, :dbname, :db, and :sock will be deprecated at some point in the future."
69
+ warn "The options :user, :pass, :hostname, :dbname, :db, and :sock are deprecated and will be removed at some point in the future."
50
70
  warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options."
51
71
  warn "============= END WARNING FROM mysql2 ========="
52
72
  end
@@ -57,7 +77,6 @@ module Mysql2
57
77
  port = opts[:port]
58
78
  database = opts[:database] || opts[:dbname] || opts[:db]
59
79
  socket = opts[:socket] || opts[:sock]
60
- flags = opts[:flags] ? opts[:flags] | @query_options[:connect_flags] : @query_options[:connect_flags]
61
80
 
62
81
  # Correct the data types before passing these values down to the C level
63
82
  user = user.to_s unless user.nil?
@@ -70,8 +89,41 @@ module Mysql2
70
89
  connect user, pass, host, port, database, socket, flags
71
90
  end
72
91
 
73
- def self.default_query_options
74
- @@default_query_options
92
+ def parse_ssl_mode(mode)
93
+ m = mode.to_s.upcase
94
+ if m.start_with?('SSL_MODE_')
95
+ return Mysql2::Client.const_get(m) if Mysql2::Client.const_defined?(m)
96
+ else
97
+ x = 'SSL_MODE_' + m
98
+ return Mysql2::Client.const_get(x) if Mysql2::Client.const_defined?(x)
99
+ end
100
+ warn "Unknown MySQL ssl_mode flag: #{mode}"
101
+ end
102
+
103
+ def parse_flags_array(flags, initial = 0)
104
+ flags.reduce(initial) do |memo, f|
105
+ fneg = f.start_with?('-') ? f[1..-1] : nil
106
+ if fneg && fneg =~ /^\w+$/ && Mysql2::Client.const_defined?(fneg)
107
+ memo & ~ Mysql2::Client.const_get(fneg)
108
+ elsif f && f =~ /^\w+$/ && Mysql2::Client.const_defined?(f)
109
+ memo | Mysql2::Client.const_get(f)
110
+ else
111
+ warn "Unknown MySQL connection flag: '#{f}'"
112
+ memo
113
+ end
114
+ end
115
+ end
116
+
117
+ if Thread.respond_to?(:handle_interrupt)
118
+ def query(sql, options = {})
119
+ Thread.handle_interrupt(::Mysql2::Util::TimeoutError => :never) do
120
+ _query(sql, @query_options.merge(options))
121
+ end
122
+ end
123
+ else
124
+ def query(sql, options = {})
125
+ _query(sql, @query_options.merge(options))
126
+ end
75
127
  end
76
128
 
77
129
  def query_info
@@ -82,9 +134,16 @@ module Mysql2
82
134
  info_hash
83
135
  end
84
136
 
85
- private
86
- def self.local_offset
137
+ def info
138
+ self.class.info
139
+ end
140
+
141
+ class << self
142
+ private
143
+
144
+ def local_offset
87
145
  ::Time.local(2010).utc_offset.to_r / 86400
88
146
  end
147
+ end
89
148
  end
90
149
  end
@@ -1,5 +1,5 @@
1
1
  # Loaded by script/console. Land helpers here.
2
2
 
3
- Pry.config.prompt = lambda do |context, nesting, pry|
3
+ Pry.config.prompt = lambda do |context, *|
4
4
  "[mysql2] #{context}> "
5
5
  end
data/lib/mysql2/em.rb CHANGED
@@ -17,7 +17,7 @@ module Mysql2
17
17
  detach
18
18
  begin
19
19
  result = @client.async_result
20
- rescue Exception => e
20
+ rescue => e
21
21
  @deferable.fail(e)
22
22
  else
23
23
  @deferable.succeed(result)
@@ -34,17 +34,16 @@ module Mysql2
34
34
  end
35
35
 
36
36
  def close(*args)
37
- if @watch
38
- @watch.detach if @watch.watching?
39
- end
37
+ @watch.detach if @watch && @watch.watching?
38
+
40
39
  super(*args)
41
40
  end
42
41
 
43
- def query(sql, opts={})
42
+ def query(sql, opts = {})
44
43
  if ::EM.reactor_running?
45
44
  super(sql, opts.merge(:async => true))
46
45
  deferable = ::EM::DefaultDeferrable.new
47
- @watch = ::EM.watch(self.socket, Watcher, self, deferable)
46
+ @watch = ::EM.watch(socket, Watcher, self, deferable)
48
47
  @watch.notify_readable = true
49
48
  deferable
50
49
  else
data/lib/mysql2/error.rb CHANGED
@@ -2,25 +2,31 @@
2
2
 
3
3
  module Mysql2
4
4
  class Error < StandardError
5
- REPLACEMENT_CHAR = '?'
6
- ENCODE_OPTS = {:undef => :replace, :invalid => :replace, :replace => REPLACEMENT_CHAR}
5
+ ENCODE_OPTS = {
6
+ :undef => :replace,
7
+ :invalid => :replace,
8
+ :replace => '?'.freeze,
9
+ }.freeze
7
10
 
8
- attr_accessor :error_number
9
- attr_reader :sql_state
10
- attr_writer :server_version
11
+ attr_reader :error_number, :sql_state
11
12
 
12
13
  # Mysql gem compatibility
13
14
  alias_method :errno, :error_number
14
15
  alias_method :error, :message
15
16
 
16
- def initialize(msg, server_version=nil)
17
- self.server_version = server_version
17
+ def initialize(msg)
18
+ @server_version ||= nil
18
19
 
19
20
  super(clean_message(msg))
20
21
  end
21
22
 
22
- def sql_state=(state)
23
- @sql_state = ''.respond_to?(:encode) ? state.encode(ENCODE_OPTS) : state
23
+ def self.new_with_args(msg, server_version, error_number, sql_state)
24
+ err = allocate
25
+ err.instance_variable_set('@server_version', server_version)
26
+ err.instance_variable_set('@error_number', error_number)
27
+ err.instance_variable_set('@sql_state', sql_state.respond_to?(:encode) ? sql_state.encode(ENCODE_OPTS) : sql_state)
28
+ err.send(:initialize, msg)
29
+ err
24
30
  end
25
31
 
26
32
  private
@@ -30,7 +36,7 @@ module Mysql2
30
36
  # variable.
31
37
  #
32
38
  # See http://dev.mysql.com/doc/refman/5.5/en/charset-errors.html for
33
- # more contetx.
39
+ # more context.
34
40
  #
35
41
  # Before MySQL 5.5 error message template strings are in whatever encoding
36
42
  # is associated with the error message language.
@@ -53,27 +59,12 @@ module Mysql2
53
59
  #
54
60
  # Returns a valid UTF-8 string in Ruby 1.9+, the original string on Ruby 1.8
55
61
  def clean_message(message)
56
- return message if !message.respond_to?(:encoding)
62
+ return message unless message.respond_to?(:encode)
57
63
 
58
64
  if @server_version && @server_version > 50500
59
65
  message.encode(ENCODE_OPTS)
60
66
  else
61
- if message.respond_to? :scrub
62
- message.scrub(REPLACEMENT_CHAR).encode(ENCODE_OPTS)
63
- else
64
- # This is ugly as hell but Ruby 1.9 doesn't provide a way to clean a string
65
- # and retain it's valid UTF-8 characters, that I know of.
66
-
67
- new_message = "".force_encoding(Encoding::UTF_8)
68
- message.chars.each do |char|
69
- if char.valid_encoding?
70
- new_message << char
71
- else
72
- new_message << REPLACEMENT_CHAR
73
- end
74
- end
75
- new_message.encode(ENCODE_OPTS)
76
- end
67
+ message.encode(Encoding::UTF_8, ENCODE_OPTS)
77
68
  end
78
69
  end
79
70
  end
@@ -0,0 +1,3 @@
1
+ module Mysql2
2
+ Field = Struct.new(:name, :type)
3
+ end
@@ -0,0 +1,17 @@
1
+ module Mysql2
2
+ class Statement
3
+ include Enumerable
4
+
5
+ if Thread.respond_to?(:handle_interrupt)
6
+ def execute(*args)
7
+ Thread.handle_interrupt(::Mysql2::Util::TimeoutError => :never) do
8
+ _execute(*args)
9
+ end
10
+ end
11
+ else
12
+ def execute(*args)
13
+ _execute(*args)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Mysql2
2
- VERSION = "0.3.18"
2
+ VERSION = "0.4.7"
3
3
  end
data/lib/mysql2.rb CHANGED
@@ -8,16 +8,16 @@ require 'rational' unless RUBY_VERSION >= '1.9.2'
8
8
  # Or to bomb out with a clear error message instead of a linker crash
9
9
  if RUBY_PLATFORM =~ /mswin|mingw/
10
10
  dll_path = if ENV['RUBY_MYSQL2_LIBMYSQL_DLL']
11
- # If this environment variable is set, it overrides any other paths
12
- # The user is advised to use backslashes not forward slashes
13
- ENV['RUBY_MYSQL2_LIBMYSQL_DLL'].dup
14
- elsif File.exist?(File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)))
15
- # Use vendor/libmysql.dll if it exists, convert slashes for Win32 LoadLibrary
16
- File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)).gsub('/', '\\')
17
- else
18
- # This will use default / system library paths
19
- 'libmysql.dll'
20
- end
11
+ # If this environment variable is set, it overrides any other paths
12
+ # The user is advised to use backslashes not forward slashes
13
+ ENV['RUBY_MYSQL2_LIBMYSQL_DLL']
14
+ elsif File.exist?(File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)))
15
+ # Use vendor/libmysql.dll if it exists, convert slashes for Win32 LoadLibrary
16
+ File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)).tr('/', '\\')
17
+ else
18
+ # This will use default / system library paths
19
+ 'libmysql.dll'
20
+ end
21
21
 
22
22
  require 'Win32API'
23
23
  LoadLibrary = Win32API.new('Kernel32', 'LoadLibrary', ['P'], 'I')
@@ -31,6 +31,8 @@ require 'mysql2/error'
31
31
  require 'mysql2/mysql2'
32
32
  require 'mysql2/result'
33
33
  require 'mysql2/client'
34
+ require 'mysql2/field'
35
+ require 'mysql2/statement'
34
36
 
35
37
  # = Mysql2
36
38
  #
@@ -51,14 +53,32 @@ if defined?(ActiveRecord::VERSION::STRING) && ActiveRecord::VERSION::STRING < "3
51
53
  end
52
54
 
53
55
  # For holding utility methods
54
- module Mysql2::Util
56
+ module Mysql2
57
+ module Util
58
+ #
59
+ # Rekey a string-keyed hash with equivalent symbols.
60
+ #
61
+ def self.key_hash_as_symbols(hash)
62
+ return nil unless hash
63
+ Hash[hash.map { |k, v| [k.to_sym, v] }]
64
+ end
55
65
 
56
- #
57
- # Rekey a string-keyed hash with equivalent symbols.
58
- #
59
- def self.key_hash_as_symbols(hash)
60
- return nil unless hash
61
- Hash[hash.map { |k,v| [k.to_sym, v] }]
66
+ #
67
+ # In Mysql2::Client#query and Mysql2::Statement#execute,
68
+ # Thread#handle_interrupt is used to prevent Timeout#timeout
69
+ # from interrupting query execution.
70
+ #
71
+ # Timeout::ExitException was removed in Ruby 2.3.0, 2.2.3, and 2.1.8,
72
+ # but is present in earlier 2.1.x and 2.2.x, so we provide a shim.
73
+ #
74
+ if Thread.respond_to?(:handle_interrupt)
75
+ require 'timeout'
76
+ # rubocop:disable Style/ConstantName
77
+ TimeoutError = if defined?(::Timeout::ExitException)
78
+ ::Timeout::ExitException
79
+ else
80
+ ::Timeout::Error
81
+ end
82
+ end
62
83
  end
63
-
64
84
  end
@@ -9,9 +9,3 @@ user:
9
9
  username: LOCALUSERNAME
10
10
  password:
11
11
  database: mysql2_test
12
-
13
- numericuser:
14
- host: localhost
15
- username: LOCALUSERNAME
16
- password:
17
- database: 12345
data/spec/em/em_spec.rb CHANGED
@@ -4,7 +4,7 @@ begin
4
4
  require 'eventmachine'
5
5
  require 'mysql2/em'
6
6
 
7
- describe Mysql2::EM::Client do
7
+ RSpec.describe Mysql2::EM::Client do
8
8
  it "should support async queries" do
9
9
  results = []
10
10
  EM.run do
@@ -24,8 +24,8 @@ begin
24
24
  end
25
25
  end
26
26
 
27
- results[0].keys.should include("second_query")
28
- results[1].keys.should include("first_query")
27
+ expect(results[0].keys).to include("second_query")
28
+ expect(results[1].keys).to include("first_query")
29
29
  end
30
30
 
31
31
  it "should support queries in callbacks" do
@@ -44,38 +44,39 @@ begin
44
44
  end
45
45
  end
46
46
 
47
- results[0].keys.should include("first_query")
48
- results[1].keys.should include("second_query")
47
+ expect(results[0].keys).to include("first_query")
48
+ expect(results[1].keys).to include("second_query")
49
49
  end
50
50
 
51
51
  it "should not swallow exceptions raised in callbacks" do
52
- lambda {
52
+ expect {
53
53
  EM.run do
54
54
  client = Mysql2::EM::Client.new DatabaseCredentials['root']
55
55
  defer = client.query "SELECT sleep(0.1) as first_query"
56
- defer.callback do |result|
56
+ defer.callback do
57
57
  client.close
58
- raise 'some error'
58
+ fail 'some error'
59
59
  end
60
- defer.errback do |err|
60
+ defer.errback do
61
61
  # This _shouldn't_ be run, but it needed to prevent the specs from
62
62
  # freezing if this test fails.
63
63
  EM.stop_event_loop
64
64
  end
65
65
  end
66
- }.should raise_error
66
+ }.to raise_error('some error')
67
67
  end
68
68
 
69
69
  context 'when an exception is raised by the client' do
70
70
  let(:client) { Mysql2::EM::Client.new DatabaseCredentials['root'] }
71
71
  let(:error) { StandardError.new('some error') }
72
- before { client.stub(:async_result).and_raise(error) }
72
+ before { allow(client).to receive(:async_result).and_raise(error) }
73
+ after { client.close }
73
74
 
74
75
  it "should swallow exceptions raised in by the client" do
75
76
  errors = []
76
77
  EM.run do
77
78
  defer = client.query "SELECT sleep(0.1) as first_query"
78
- defer.callback do |result|
79
+ defer.callback do
79
80
  # This _shouldn't_ be run, but it is needed to prevent the specs from
80
81
  # freezing if this test fails.
81
82
  EM.stop_event_loop
@@ -85,7 +86,7 @@ begin
85
86
  EM.stop_event_loop
86
87
  end
87
88
  end
88
- errors.should == [error]
89
+ expect(errors).to eq([error])
89
90
  end
90
91
 
91
92
  it "should fail the deferrable" do
@@ -93,19 +94,19 @@ begin
93
94
  EM.run do
94
95
  defer = client.query "SELECT sleep(0.025) as first_query"
95
96
  EM.add_timer(0.1) do
96
- defer.callback do |result|
97
+ defer.callback do
97
98
  callbacks_run << :callback
98
99
  # This _shouldn't_ be run, but it is needed to prevent the specs from
99
100
  # freezing if this test fails.
100
101
  EM.stop_event_loop
101
102
  end
102
- defer.errback do |err|
103
+ defer.errback do
103
104
  callbacks_run << :errback
104
105
  EM.stop_event_loop
105
106
  end
106
107
  end
107
108
  end
108
- callbacks_run.should == [:errback]
109
+ expect(callbacks_run).to eq([:errback])
109
110
  end
110
111
  end
111
112
 
@@ -114,17 +115,17 @@ begin
114
115
  EM.run do
115
116
  client = Mysql2::EM::Client.new DatabaseCredentials['root']
116
117
  defer = client.query("select sleep(0.025)")
117
- defer.callback do |result|
118
+ defer.callback do
118
119
  callbacks_run << :callback
119
120
  end
120
- defer.errback do |err|
121
+ defer.errback do
121
122
  callbacks_run << :errback
122
123
  end
123
124
  EM.add_timer(0.1) do
124
- callbacks_run.should == [:callback]
125
- lambda {
125
+ expect(callbacks_run).to eq([:callback])
126
+ expect {
126
127
  client.close
127
- }.should_not raise_error(/invalid binding to detach/)
128
+ }.not_to raise_error
128
129
  EM.stop_event_loop
129
130
  end
130
131
  end