mysql2 0.1.4 → 0.1.5
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.
- data/CHANGELOG.md +19 -0
- data/README.rdoc +4 -4
- data/VERSION +1 -1
- data/benchmark/active_record.rb +17 -5
- data/benchmark/query_with_mysql_casting.rb +82 -0
- data/benchmark/{query.rb → query_without_mysql_casting.rb} +0 -0
- data/benchmark/sequel.rb +39 -0
- data/ext/extconf.rb +3 -0
- data/ext/mysql2_ext.c +234 -66
- data/ext/mysql2_ext.h +69 -9
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +25 -8
- data/lib/mysql2.rb +1 -1
- data/lib/sequel/adapters/mysql2.rb +238 -0
- data/mysql2.gemspec +6 -3
- data/spec/active_record/active_record_spec.rb +124 -1
- data/spec/mysql2/client_spec.rb +15 -0
- data/spec/mysql2/result_spec.rb +89 -42
- metadata +7 -4
data/ext/mysql2_ext.h
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
#include <time.h>
|
2
1
|
#include <ruby.h>
|
2
|
+
#include <fcntl.h>
|
3
3
|
|
4
4
|
#ifdef HAVE_MYSQL_H
|
5
5
|
#include <mysql.h>
|
@@ -15,17 +15,26 @@
|
|
15
15
|
|
16
16
|
#ifdef HAVE_RUBY_ENCODING_H
|
17
17
|
#include <ruby/encoding.h>
|
18
|
-
int utf8Encoding, binaryEncoding;
|
18
|
+
static int utf8Encoding, binaryEncoding;
|
19
|
+
#endif
|
20
|
+
|
21
|
+
#if defined(__GNUC__) && (__GNUC__ >= 3)
|
22
|
+
#define RB_MYSQL_UNUSED __attribute__ ((unused))
|
23
|
+
#else
|
24
|
+
#define RB_MYSQL_UNUSED
|
19
25
|
#endif
|
20
26
|
|
21
27
|
static VALUE cBigDecimal, cDate, cDateTime;
|
22
|
-
ID intern_new, intern_local;
|
28
|
+
static ID intern_new, intern_local;
|
23
29
|
|
24
30
|
/* Mysql2::Error */
|
25
|
-
VALUE cMysql2Error;
|
31
|
+
static VALUE cMysql2Error;
|
26
32
|
|
27
33
|
/* Mysql2::Client */
|
28
|
-
|
34
|
+
typedef struct {
|
35
|
+
MYSQL * client;
|
36
|
+
} mysql2_client_wrapper;
|
37
|
+
#define GetMysql2Client(obj, sval) (sval = ((mysql2_client_wrapper*)(DATA_PTR(obj)))->client);
|
29
38
|
static ID sym_socket, sym_host, sym_port, sym_username, sym_password,
|
30
39
|
sym_database, sym_reconnect, sym_connect_timeout, sym_id, sym_version,
|
31
40
|
sym_sslkey, sym_sslcert, sym_sslca, sym_sslcapath, sym_sslcipher,
|
@@ -40,7 +49,7 @@ static VALUE rb_mysql_client_socket(VALUE self);
|
|
40
49
|
static VALUE rb_mysql_client_async_result(VALUE self);
|
41
50
|
static VALUE rb_mysql_client_last_id(VALUE self);
|
42
51
|
static VALUE rb_mysql_client_affected_rows(VALUE self);
|
43
|
-
void rb_mysql_client_free(void * client);
|
52
|
+
static void rb_mysql_client_free(void * client);
|
44
53
|
|
45
54
|
/* Mysql2::Result */
|
46
55
|
typedef struct {
|
@@ -53,9 +62,60 @@ typedef struct {
|
|
53
62
|
MYSQL_RES *result;
|
54
63
|
} mysql2_result_wrapper;
|
55
64
|
#define GetMysql2Result(obj, sval) (sval = (mysql2_result_wrapper*)DATA_PTR(obj));
|
56
|
-
VALUE cMysql2Result;
|
65
|
+
static VALUE cMysql2Result;
|
57
66
|
static VALUE rb_mysql_result_to_obj(MYSQL_RES * res);
|
58
67
|
static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self);
|
59
68
|
static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self);
|
60
|
-
void rb_mysql_result_free(void * wrapper);
|
61
|
-
void rb_mysql_result_mark(void * wrapper);
|
69
|
+
static void rb_mysql_result_free(void * wrapper);
|
70
|
+
static void rb_mysql_result_mark(void * wrapper);
|
71
|
+
static void rb_mysql_result_free_result(mysql2_result_wrapper * wrapper);
|
72
|
+
|
73
|
+
/*
|
74
|
+
* used to pass all arguments to mysql_real_connect while inside
|
75
|
+
* rb_thread_blocking_region
|
76
|
+
*/
|
77
|
+
struct nogvl_connect_args {
|
78
|
+
MYSQL *mysql;
|
79
|
+
const char *host;
|
80
|
+
const char *user;
|
81
|
+
const char *passwd;
|
82
|
+
const char *db;
|
83
|
+
unsigned int port;
|
84
|
+
const char *unix_socket;
|
85
|
+
unsigned long client_flag;
|
86
|
+
};
|
87
|
+
|
88
|
+
/*
|
89
|
+
* used to pass all arguments to mysql_send_query while inside
|
90
|
+
* rb_thread_blocking_region
|
91
|
+
*/
|
92
|
+
struct nogvl_send_query_args {
|
93
|
+
MYSQL *mysql;
|
94
|
+
VALUE sql;
|
95
|
+
};
|
96
|
+
|
97
|
+
/*
|
98
|
+
* partial emulation of the 1.9 rb_thread_blocking_region under 1.8,
|
99
|
+
* this is enough for dealing with blocking I/O functions in the
|
100
|
+
* presence of threads.
|
101
|
+
*/
|
102
|
+
#ifndef HAVE_RB_THREAD_BLOCKING_REGION
|
103
|
+
# include <rubysig.h>
|
104
|
+
# define RUBY_UBF_IO ((rb_unblock_function_t *)-1)
|
105
|
+
typedef void rb_unblock_function_t(void *);
|
106
|
+
typedef VALUE rb_blocking_function_t(void *);
|
107
|
+
static VALUE
|
108
|
+
rb_thread_blocking_region(
|
109
|
+
rb_blocking_function_t *func, void *data1,
|
110
|
+
RB_MYSQL_UNUSED rb_unblock_function_t *ubf,
|
111
|
+
RB_MYSQL_UNUSED void *data2)
|
112
|
+
{
|
113
|
+
VALUE rv;
|
114
|
+
|
115
|
+
TRAP_BEG;
|
116
|
+
rv = func(data1);
|
117
|
+
TRAP_END;
|
118
|
+
|
119
|
+
return rv;
|
120
|
+
}
|
121
|
+
#endif /* ! HAVE_RB_THREAD_BLOCKING_REGION */
|
@@ -13,6 +13,7 @@ module ActiveRecord
|
|
13
13
|
|
14
14
|
module ConnectionAdapters
|
15
15
|
class Mysql2Column < Column
|
16
|
+
BOOL = "tinyint(1)".freeze
|
16
17
|
def extract_default(default)
|
17
18
|
if sql_type =~ /blob/i || type == :text
|
18
19
|
if default.blank?
|
@@ -39,7 +40,7 @@ module ActiveRecord
|
|
39
40
|
when :float then Float
|
40
41
|
when :decimal then BigDecimal
|
41
42
|
when :datetime then Time
|
42
|
-
when :date then
|
43
|
+
when :date then Date
|
43
44
|
when :timestamp then Time
|
44
45
|
when :time then Time
|
45
46
|
when :text, :string then String
|
@@ -49,21 +50,36 @@ module ActiveRecord
|
|
49
50
|
end
|
50
51
|
|
51
52
|
def type_cast(value)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
value
|
53
|
+
return nil if value.nil?
|
54
|
+
case type
|
55
|
+
when :string then value
|
56
|
+
when :text then value
|
57
|
+
when :integer then value.is_a?(Fixnum) ? value : (value.to_i rescue value ? 1 : 0)
|
58
|
+
when :float then value.class == Float ? value : value.to_f
|
59
|
+
when :decimal then value.class == BigDecimal ? value : self.class.value_to_decimal(value)
|
60
|
+
when :datetime then value.class == Time ? value : self.class.string_to_time(value)
|
61
|
+
when :timestamp then value.class == Time ? value : self.class.string_to_time(value)
|
62
|
+
when :time then value.class == Time ? value : self.class.string_to_dummy_time(value)
|
63
|
+
when :date then value.class == Date ? value : self.class.string_to_date(value)
|
64
|
+
when :binary then value
|
65
|
+
when :boolean then self.class.value_to_boolean(value)
|
66
|
+
else value
|
56
67
|
end
|
57
68
|
end
|
58
69
|
|
59
70
|
def type_cast_code(var_name)
|
60
|
-
|
71
|
+
case type
|
72
|
+
when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})"
|
73
|
+
else
|
74
|
+
nil
|
75
|
+
end
|
61
76
|
end
|
62
77
|
|
63
78
|
private
|
64
79
|
def simplified_type(field_type)
|
65
|
-
return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(
|
80
|
+
return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL)
|
66
81
|
return :string if field_type =~ /enum/i
|
82
|
+
return :integer if field_type =~ /year/i
|
67
83
|
super
|
68
84
|
end
|
69
85
|
|
@@ -107,6 +123,7 @@ module ActiveRecord
|
|
107
123
|
self.emulate_booleans = true
|
108
124
|
|
109
125
|
ADAPTER_NAME = 'MySQL'.freeze
|
126
|
+
PRIMARY = "PRIMARY".freeze
|
110
127
|
|
111
128
|
LOST_CONNECTION_ERROR_MESSAGES = [
|
112
129
|
"Server shutdown in progress",
|
@@ -479,7 +496,7 @@ module ActiveRecord
|
|
479
496
|
|
480
497
|
def show_variable(name)
|
481
498
|
variables = select_all("SHOW VARIABLES LIKE '#{name}'")
|
482
|
-
variables.first[
|
499
|
+
variables.first['Value'] unless variables.empty?
|
483
500
|
end
|
484
501
|
|
485
502
|
def pk_and_sequence_for(table)
|
data/lib/mysql2.rb
CHANGED
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'mysql2' unless defined? Mysql2
|
2
|
+
|
3
|
+
Sequel.require %w'shared/mysql utils/stored_procedures', 'adapters'
|
4
|
+
|
5
|
+
module Sequel
|
6
|
+
# Module for holding all MySQL-related classes and modules for Sequel.
|
7
|
+
module Mysql2
|
8
|
+
# Mapping of type numbers to conversion procs
|
9
|
+
MYSQL_TYPES = {}
|
10
|
+
|
11
|
+
MYSQL2_LITERAL_PROC = lambda{|v| v}
|
12
|
+
|
13
|
+
# Use only a single proc for each type to save on memory
|
14
|
+
MYSQL_TYPE_PROCS = {
|
15
|
+
[0, 246] => MYSQL2_LITERAL_PROC, # decimal
|
16
|
+
[1] => lambda{|v| convert_tinyint_to_bool ? v != 0 : v}, # tinyint
|
17
|
+
[2, 3, 8, 9, 13, 247, 248] => MYSQL2_LITERAL_PROC, # integer
|
18
|
+
[4, 5] => MYSQL2_LITERAL_PROC, # float
|
19
|
+
[10, 14] => MYSQL2_LITERAL_PROC, # date
|
20
|
+
[7, 12] => MYSQL2_LITERAL_PROC, # datetime
|
21
|
+
[11] => MYSQL2_LITERAL_PROC, # time
|
22
|
+
[249, 250, 251, 252] => lambda{|v| Sequel::SQL::Blob.new(v)} # blob
|
23
|
+
}
|
24
|
+
MYSQL_TYPE_PROCS.each do |k,v|
|
25
|
+
k.each{|n| MYSQL_TYPES[n] = v}
|
26
|
+
end
|
27
|
+
|
28
|
+
@convert_invalid_date_time = false
|
29
|
+
@convert_tinyint_to_bool = true
|
30
|
+
|
31
|
+
class << self
|
32
|
+
# By default, Sequel raises an exception if in invalid date or time is used.
|
33
|
+
# However, if this is set to nil or :nil, the adapter treats dates
|
34
|
+
# like 0000-00-00 and times like 838:00:00 as nil values. If set to :string,
|
35
|
+
# it returns the strings as is.
|
36
|
+
attr_accessor :convert_invalid_date_time
|
37
|
+
|
38
|
+
# Sequel converts the column type tinyint(1) to a boolean by default when
|
39
|
+
# using the native MySQL adapter. You can turn off the conversion by setting
|
40
|
+
# this to false.
|
41
|
+
attr_accessor :convert_tinyint_to_bool
|
42
|
+
end
|
43
|
+
|
44
|
+
# Database class for MySQL databases used with Sequel.
|
45
|
+
class Database < Sequel::Database
|
46
|
+
include Sequel::MySQL::DatabaseMethods
|
47
|
+
|
48
|
+
# Mysql::Error messages that indicate the current connection should be disconnected
|
49
|
+
MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now|Can't connect to local MySQL server through socket|MySQL server has gone away)/
|
50
|
+
|
51
|
+
set_adapter_scheme :mysql2
|
52
|
+
|
53
|
+
# Support stored procedures on MySQL
|
54
|
+
def call_sproc(name, opts={}, &block)
|
55
|
+
args = opts[:args] || []
|
56
|
+
execute("CALL #{name}#{args.empty? ? '()' : literal(args)}", opts.merge(:sproc=>false), &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Connect to the database. In addition to the usual database options,
|
60
|
+
# the following options have effect:
|
61
|
+
#
|
62
|
+
# * :auto_is_null - Set to true to use MySQL default behavior of having
|
63
|
+
# a filter for an autoincrement column equals NULL to return the last
|
64
|
+
# inserted row.
|
65
|
+
# * :charset - Same as :encoding (:encoding takes precendence)
|
66
|
+
# * :compress - Set to false to not compress results from the server
|
67
|
+
# * :config_default_group - The default group to read from the in
|
68
|
+
# the MySQL config file.
|
69
|
+
# * :config_local_infile - If provided, sets the Mysql::OPT_LOCAL_INFILE
|
70
|
+
# option on the connection with the given value.
|
71
|
+
# * :encoding - Set all the related character sets for this
|
72
|
+
# connection (connection, client, database, server, and results).
|
73
|
+
# * :socket - Use a unix socket file instead of connecting via TCP/IP.
|
74
|
+
# * :timeout - Set the timeout in seconds before the server will
|
75
|
+
# disconnect this connection.
|
76
|
+
def connect(server)
|
77
|
+
opts = server_opts(server)
|
78
|
+
conn = ::Mysql2::Client.new({
|
79
|
+
:host => opts[:host] || 'localhost',
|
80
|
+
:username => opts[:user],
|
81
|
+
:password => opts[:password],
|
82
|
+
:database => opts[:database],
|
83
|
+
:port => opts[:port],
|
84
|
+
:socket => opts[:socket]
|
85
|
+
})
|
86
|
+
|
87
|
+
# increase timeout so mysql server doesn't disconnect us
|
88
|
+
conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}")
|
89
|
+
|
90
|
+
# By default, MySQL 'where id is null' selects the last inserted id
|
91
|
+
conn.query("set SQL_AUTO_IS_NULL=0") unless opts[:auto_is_null]
|
92
|
+
|
93
|
+
conn
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns instance of Sequel::MySQL::Dataset with the given options.
|
97
|
+
def dataset(opts = nil)
|
98
|
+
Mysql2::Dataset.new(self, opts)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Executes the given SQL using an available connection, yielding the
|
102
|
+
# connection if the block is given.
|
103
|
+
def execute(sql, opts={}, &block)
|
104
|
+
if opts[:sproc]
|
105
|
+
call_sproc(sql, opts, &block)
|
106
|
+
else
|
107
|
+
synchronize(opts[:server]){|conn| _execute(conn, sql, opts, &block)}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Return the version of the MySQL server two which we are connecting.
|
112
|
+
def server_version(server=nil)
|
113
|
+
@server_version ||= (synchronize(server){|conn| conn.info[:id]})
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# Execute the given SQL on the given connection. If the :type
|
119
|
+
# option is :select, yield the result of the query, otherwise
|
120
|
+
# yield the connection if a block is given.
|
121
|
+
def _execute(conn, sql, opts)
|
122
|
+
begin
|
123
|
+
# r = log_yield(sql){conn.query(sql)}
|
124
|
+
r = conn.query(sql)
|
125
|
+
if opts[:type] == :select
|
126
|
+
yield r if r
|
127
|
+
elsif block_given?
|
128
|
+
yield conn
|
129
|
+
end
|
130
|
+
rescue ::Mysql2::Error => e
|
131
|
+
raise_error(e, :disconnect=>MYSQL_DATABASE_DISCONNECT_ERRORS.match(e.message))
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# MySQL connections use the query method to execute SQL without a result
|
136
|
+
def connection_execute_method
|
137
|
+
:query
|
138
|
+
end
|
139
|
+
|
140
|
+
# The MySQL adapter main error class is Mysql::Error
|
141
|
+
def database_error_classes
|
142
|
+
[::Mysql2::Error]
|
143
|
+
end
|
144
|
+
|
145
|
+
# The database name when using the native adapter is always stored in
|
146
|
+
# the :database option.
|
147
|
+
def database_name
|
148
|
+
@opts[:database]
|
149
|
+
end
|
150
|
+
|
151
|
+
# Closes given database connection.
|
152
|
+
def disconnect_connection(c)
|
153
|
+
c = nil
|
154
|
+
end
|
155
|
+
|
156
|
+
# Convert tinyint(1) type to boolean if convert_tinyint_to_bool is true
|
157
|
+
def schema_column_type(db_type)
|
158
|
+
Sequel::MySQL.convert_tinyint_to_bool && db_type == 'tinyint(1)' ? :boolean : super
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Dataset class for MySQL datasets accessed via the native driver.
|
163
|
+
class Dataset < Sequel::Dataset
|
164
|
+
include Sequel::MySQL::DatasetMethods
|
165
|
+
include StoredProcedures
|
166
|
+
|
167
|
+
# Methods for MySQL stored procedures using the native driver.
|
168
|
+
module StoredProcedureMethods
|
169
|
+
include Sequel::Dataset::StoredProcedureMethods
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
# Execute the database stored procedure with the stored arguments.
|
174
|
+
def execute(sql, opts={}, &block)
|
175
|
+
super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Same as execute, explicit due to intricacies of alias and super.
|
179
|
+
def execute_dui(sql, opts={}, &block)
|
180
|
+
super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Delete rows matching this dataset
|
185
|
+
def delete
|
186
|
+
execute_dui(delete_sql){|c| return c.affected_rows}
|
187
|
+
end
|
188
|
+
|
189
|
+
# Yield all rows matching this dataset. If the dataset is set to
|
190
|
+
# split multiple statements, yield arrays of hashes one per statement
|
191
|
+
# instead of yielding results for all statements as hashes.
|
192
|
+
def fetch_rows(sql, &block)
|
193
|
+
execute(sql) do |r|
|
194
|
+
r.each &block
|
195
|
+
end
|
196
|
+
self
|
197
|
+
end
|
198
|
+
|
199
|
+
# Don't allow graphing a dataset that splits multiple statements
|
200
|
+
def graph(*)
|
201
|
+
raise(Error, "Can't graph a dataset that splits multiple result sets") if opts[:split_multiple_result_sets]
|
202
|
+
super
|
203
|
+
end
|
204
|
+
|
205
|
+
# Insert a new value into this dataset
|
206
|
+
def insert(*values)
|
207
|
+
execute_dui(insert_sql(*values)){|c| return c.insert_id}
|
208
|
+
end
|
209
|
+
|
210
|
+
# Replace (update or insert) the matching row.
|
211
|
+
def replace(*args)
|
212
|
+
execute_dui(replace_sql(*args)){|c| return c.insert_id}
|
213
|
+
end
|
214
|
+
|
215
|
+
# Update the matching rows.
|
216
|
+
def update(values={})
|
217
|
+
execute_dui(update_sql(values)){|c| return c.affected_rows}
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
# Set the :type option to :select if it hasn't been set.
|
223
|
+
def execute(sql, opts={}, &block)
|
224
|
+
super(sql, {:type=>:select}.merge(opts), &block)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Set the :type option to :dui if it hasn't been set.
|
228
|
+
def execute_dui(sql, opts={}, &block)
|
229
|
+
super(sql, {:type=>:dui}.merge(opts), &block)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Handle correct quoting of strings using ::MySQL.quote.
|
233
|
+
def literal_string(v)
|
234
|
+
db.synchronize{|c| "'#{c.quote(v)}'"}
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
data/mysql2.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{mysql2}
|
8
|
-
s.version = "0.1.
|
8
|
+
s.version = "0.1.5"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Brian Lopez"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-05-12}
|
13
13
|
s.email = %q{seniorlopez@gmail.com}
|
14
14
|
s.extensions = ["ext/extconf.rb"]
|
15
15
|
s.extra_rdoc_files = [
|
@@ -24,7 +24,9 @@ Gem::Specification.new do |s|
|
|
24
24
|
"VERSION",
|
25
25
|
"benchmark/active_record.rb",
|
26
26
|
"benchmark/escape.rb",
|
27
|
-
"benchmark/
|
27
|
+
"benchmark/query_with_mysql_casting.rb",
|
28
|
+
"benchmark/query_without_mysql_casting.rb",
|
29
|
+
"benchmark/sequel.rb",
|
28
30
|
"benchmark/setup_db.rb",
|
29
31
|
"examples/eventmachine.rb",
|
30
32
|
"ext/extconf.rb",
|
@@ -34,6 +36,7 @@ Gem::Specification.new do |s|
|
|
34
36
|
"lib/arel/engines/sql/compilers/mysql2_compiler.rb",
|
35
37
|
"lib/mysql2.rb",
|
36
38
|
"lib/mysql2/em.rb",
|
39
|
+
"lib/sequel/adapters/mysql2.rb",
|
37
40
|
"mysql2.gemspec",
|
38
41
|
"spec/active_record/active_record_spec.rb",
|
39
42
|
"spec/em/em_spec.rb",
|