mongo-lyon 1.2.4

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 (87) hide show
  1. data/LICENSE.txt +190 -0
  2. data/README.md +344 -0
  3. data/Rakefile +202 -0
  4. data/bin/mongo_console +34 -0
  5. data/docs/1.0_UPGRADE.md +21 -0
  6. data/docs/CREDITS.md +123 -0
  7. data/docs/FAQ.md +116 -0
  8. data/docs/GridFS.md +158 -0
  9. data/docs/HISTORY.md +225 -0
  10. data/docs/REPLICA_SETS.md +72 -0
  11. data/docs/TUTORIAL.md +247 -0
  12. data/docs/WRITE_CONCERN.md +28 -0
  13. data/lib/mongo.rb +77 -0
  14. data/lib/mongo/collection.rb +872 -0
  15. data/lib/mongo/connection.rb +875 -0
  16. data/lib/mongo/cursor.rb +449 -0
  17. data/lib/mongo/db.rb +607 -0
  18. data/lib/mongo/exceptions.rb +68 -0
  19. data/lib/mongo/gridfs/grid.rb +106 -0
  20. data/lib/mongo/gridfs/grid_ext.rb +57 -0
  21. data/lib/mongo/gridfs/grid_file_system.rb +145 -0
  22. data/lib/mongo/gridfs/grid_io.rb +394 -0
  23. data/lib/mongo/gridfs/grid_io_fix.rb +38 -0
  24. data/lib/mongo/repl_set_connection.rb +342 -0
  25. data/lib/mongo/util/conversions.rb +89 -0
  26. data/lib/mongo/util/core_ext.rb +60 -0
  27. data/lib/mongo/util/pool.rb +185 -0
  28. data/lib/mongo/util/server_version.rb +71 -0
  29. data/lib/mongo/util/support.rb +82 -0
  30. data/lib/mongo/util/uri_parser.rb +181 -0
  31. data/lib/mongo/version.rb +3 -0
  32. data/mongo.gemspec +34 -0
  33. data/test/auxillary/1.4_features.rb +166 -0
  34. data/test/auxillary/authentication_test.rb +68 -0
  35. data/test/auxillary/autoreconnect_test.rb +41 -0
  36. data/test/auxillary/repl_set_auth_test.rb +58 -0
  37. data/test/auxillary/slave_connection_test.rb +36 -0
  38. data/test/auxillary/threaded_authentication_test.rb +101 -0
  39. data/test/bson/binary_test.rb +15 -0
  40. data/test/bson/bson_test.rb +614 -0
  41. data/test/bson/byte_buffer_test.rb +190 -0
  42. data/test/bson/hash_with_indifferent_access_test.rb +38 -0
  43. data/test/bson/json_test.rb +17 -0
  44. data/test/bson/object_id_test.rb +154 -0
  45. data/test/bson/ordered_hash_test.rb +197 -0
  46. data/test/collection_test.rb +893 -0
  47. data/test/connection_test.rb +303 -0
  48. data/test/conversions_test.rb +120 -0
  49. data/test/cursor_fail_test.rb +75 -0
  50. data/test/cursor_message_test.rb +43 -0
  51. data/test/cursor_test.rb +457 -0
  52. data/test/db_api_test.rb +715 -0
  53. data/test/db_connection_test.rb +15 -0
  54. data/test/db_test.rb +287 -0
  55. data/test/grid_file_system_test.rb +244 -0
  56. data/test/grid_io_test.rb +120 -0
  57. data/test/grid_test.rb +200 -0
  58. data/test/load/thin/load.rb +24 -0
  59. data/test/load/unicorn/load.rb +23 -0
  60. data/test/replica_sets/connect_test.rb +86 -0
  61. data/test/replica_sets/connection_string_test.rb +32 -0
  62. data/test/replica_sets/count_test.rb +35 -0
  63. data/test/replica_sets/insert_test.rb +53 -0
  64. data/test/replica_sets/pooled_insert_test.rb +55 -0
  65. data/test/replica_sets/query_secondaries.rb +96 -0
  66. data/test/replica_sets/query_test.rb +51 -0
  67. data/test/replica_sets/replication_ack_test.rb +66 -0
  68. data/test/replica_sets/rs_test_helper.rb +27 -0
  69. data/test/safe_test.rb +68 -0
  70. data/test/support/hash_with_indifferent_access.rb +199 -0
  71. data/test/support/keys.rb +45 -0
  72. data/test/support_test.rb +19 -0
  73. data/test/test_helper.rb +83 -0
  74. data/test/threading/threading_with_large_pool_test.rb +90 -0
  75. data/test/threading_test.rb +87 -0
  76. data/test/tools/auth_repl_set_manager.rb +14 -0
  77. data/test/tools/repl_set_manager.rb +266 -0
  78. data/test/unit/collection_test.rb +130 -0
  79. data/test/unit/connection_test.rb +98 -0
  80. data/test/unit/cursor_test.rb +99 -0
  81. data/test/unit/db_test.rb +96 -0
  82. data/test/unit/grid_test.rb +49 -0
  83. data/test/unit/pool_test.rb +9 -0
  84. data/test/unit/repl_set_connection_test.rb +72 -0
  85. data/test/unit/safe_test.rb +125 -0
  86. data/test/uri_test.rb +91 -0
  87. metadata +202 -0
@@ -0,0 +1,15 @@
1
+ require './test/test_helper'
2
+
3
+ class DBConnectionTest < Test::Unit::TestCase
4
+
5
+ include Mongo
6
+
7
+ def test_no_exceptions
8
+ host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost'
9
+ port = ENV['MONGO_RUBY_DRIVER_PORT'] || Connection::DEFAULT_PORT
10
+ db = Connection.new(host, port).db(MONGO_TEST_DB)
11
+ coll = db.collection('test')
12
+ coll.remove
13
+ db.get_last_error
14
+ end
15
+ end
data/test/db_test.rb ADDED
@@ -0,0 +1,287 @@
1
+ require './test/test_helper'
2
+ require 'digest/md5'
3
+ require 'stringio'
4
+ require 'logger'
5
+
6
+ class TestPKFactory
7
+ def create_pk(row)
8
+ row['_id'] ||= BSON::ObjectId.new
9
+ row
10
+ end
11
+ end
12
+
13
+ class DBTest < Test::Unit::TestCase
14
+
15
+ include Mongo
16
+
17
+ @@conn = standard_connection
18
+ @@db = @@conn.db(MONGO_TEST_DB)
19
+ @@users = @@db.collection('system.users')
20
+ @@version = @@conn.server_version
21
+
22
+ def test_close
23
+ @@conn.close
24
+ assert !@@conn.connected?
25
+ begin
26
+ @@db.collection('test').insert('a' => 1)
27
+ fail "expected 'NilClass' exception"
28
+ rescue => ex
29
+ assert_match /NilClass/, ex.to_s
30
+ ensure
31
+ @@db = standard_connection.db(MONGO_TEST_DB)
32
+ @@users = @@db.collection('system.users')
33
+ end
34
+ end
35
+
36
+ def test_logger
37
+ output = StringIO.new
38
+ logger = Logger.new(output)
39
+ logger.level = Logger::DEBUG
40
+ conn = standard_connection(:logger => logger)
41
+ assert_equal logger, conn.logger
42
+
43
+ conn.logger.debug 'testing'
44
+ assert output.string.include?('testing')
45
+ end
46
+
47
+ def test_full_coll_name
48
+ coll = @@db.collection('test')
49
+ assert_equal "#{MONGO_TEST_DB}.test", @@db.full_collection_name(coll.name)
50
+ end
51
+
52
+ def test_collection_names
53
+ @@db.collection("test").insert("foo" => 5)
54
+ @@db.collection("test.mike").insert("bar" => 0)
55
+
56
+ colls = @@db.collection_names()
57
+ assert colls.include?("test")
58
+ assert colls.include?("test.mike")
59
+ colls.each { |name|
60
+ assert !name.include?("$")
61
+ }
62
+ end
63
+
64
+ def test_collections
65
+ @@db.collection("test.durran").insert("foo" => 5)
66
+ @@db.collection("test.les").insert("bar" => 0)
67
+
68
+ colls = @@db.collections()
69
+ assert_not_nil colls.select { |coll| coll.name == "test.durran" }
70
+ assert_not_nil colls.select { |coll| coll.name == "test.les" }
71
+ assert_equal [], colls.select { |coll| coll.name == "does_not_exist" }
72
+
73
+ assert_kind_of Collection, colls[0]
74
+ end
75
+
76
+ def test_pk_factory
77
+ db = standard_connection.db(MONGO_TEST_DB, :pk => TestPKFactory.new)
78
+ coll = db.collection('test')
79
+ coll.remove
80
+
81
+ insert_id = coll.insert('name' => 'Fred', 'age' => 42)
82
+ # new id gets added to returned object
83
+ row = coll.find_one({'name' => 'Fred'})
84
+ oid = row['_id']
85
+ assert_not_nil oid
86
+ assert_equal insert_id, oid
87
+
88
+ oid = BSON::ObjectId.new
89
+ data = {'_id' => oid, 'name' => 'Barney', 'age' => 41}
90
+ coll.insert(data)
91
+ row = coll.find_one({'name' => data['name']})
92
+ db_oid = row['_id']
93
+ assert_equal oid, db_oid
94
+ assert_equal data, row
95
+
96
+ coll.remove
97
+ end
98
+
99
+ def test_pk_factory_reset
100
+ conn = standard_connection
101
+ db = conn.db(MONGO_TEST_DB)
102
+ db.pk_factory = Object.new # first time
103
+ begin
104
+ db.pk_factory = Object.new
105
+ fail "error: expected exception"
106
+ rescue => ex
107
+ assert_match /Cannot change/, ex.to_s
108
+ ensure
109
+ conn.close
110
+ end
111
+ end
112
+
113
+ def test_authenticate
114
+ @@db.add_user('spongebob', 'squarepants')
115
+ assert_raise Mongo::AuthenticationError do
116
+ assert !@@db.authenticate('nobody', 'nopassword')
117
+ end
118
+ assert_raise Mongo::AuthenticationError do
119
+ assert !@@db.authenticate('spongebob' , 'squareliederhosen')
120
+ end
121
+ assert @@db.authenticate('spongebob', 'squarepants')
122
+ @@db.logout
123
+ @@db.remove_user('spongebob')
124
+ end
125
+
126
+ def test_authenticate_with_connection_uri
127
+ @@db.add_user('spongebob', 'squarepants')
128
+ assert Mongo::Connection.from_uri("mongodb://spongebob:squarepants@#{host_port}/#{@@db.name}")
129
+
130
+ assert_raise Mongo::AuthenticationError do
131
+ Mongo::Connection.from_uri("mongodb://wrong:info@#{host_port}/#{@@db.name}")
132
+ end
133
+ end
134
+
135
+ def test_logout
136
+ assert @@db.logout
137
+ end
138
+
139
+ def test_command
140
+ assert_raise OperationFailure do
141
+ @@db.command({:non_command => 1}, :check_response => true)
142
+ end
143
+
144
+ result = @@db.command({:non_command => 1}, :check_response => false)
145
+ assert !Mongo::Support.ok?(result)
146
+ end
147
+
148
+ def test_error
149
+ @@db.reset_error_history
150
+ assert_nil @@db.get_last_error['err']
151
+ assert !@@db.error?
152
+ assert_nil @@db.previous_error
153
+
154
+ @@db.command({:forceerror => 1}, :check_response => false)
155
+ assert @@db.error?
156
+ assert_not_nil @@db.get_last_error['err']
157
+ assert_not_nil @@db.previous_error
158
+
159
+ @@db.command({:forceerror => 1}, :check_response => false)
160
+ assert @@db.error?
161
+ assert @@db.get_last_error['err']
162
+ prev_error = @@db.previous_error
163
+ assert_equal 1, prev_error['nPrev']
164
+ assert_equal prev_error["err"], @@db.get_last_error['err']
165
+
166
+ @@db.collection('test').find_one
167
+ assert_nil @@db.get_last_error['err']
168
+ assert !@@db.error?
169
+ assert @@db.previous_error
170
+ assert_equal 2, @@db.previous_error['nPrev']
171
+
172
+ @@db.reset_error_history
173
+ assert_nil @@db.get_last_error['err']
174
+ assert !@@db.error?
175
+ assert_nil @@db.previous_error
176
+ end
177
+
178
+ def test_check_command_response
179
+ command = {:forceerror => 1}
180
+ assert_raise OperationFailure do
181
+ @@db.command(command)
182
+ end
183
+ end
184
+
185
+ def test_last_status
186
+ @@db['test'].remove
187
+ @@db['test'].save("i" => 1)
188
+
189
+ @@db['test'].update({"i" => 1}, {"$set" => {"i" => 2}})
190
+ assert @@db.get_last_error()["updatedExisting"]
191
+
192
+ @@db['test'].update({"i" => 1}, {"$set" => {"i" => 500}})
193
+ assert !@@db.get_last_error()["updatedExisting"]
194
+ end
195
+
196
+ def test_text_port_number_raises_no_errors
197
+ conn = standard_connection
198
+ db = conn[MONGO_TEST_DB]
199
+ db.collection('users').remove
200
+ end
201
+
202
+ def test_user_management
203
+ @@db.add_user("bob", "secret")
204
+ assert @@db.authenticate("bob", "secret")
205
+ @@db.logout
206
+ assert @@db.remove_user("bob")
207
+ assert_raise Mongo::AuthenticationError do
208
+ @@db.authenticate("bob", "secret")
209
+ end
210
+ end
211
+
212
+ def test_remove_non_existant_user
213
+ assert !@@db.remove_user("joe")
214
+ end
215
+
216
+ def test_stored_function_management
217
+ @@db.add_stored_function("sum", "function (x, y) { return x + y; }")
218
+ assert_equal @@db.eval("return sum(2,3);"), 5
219
+ assert @@db.remove_stored_function("sum")
220
+ assert_raise OperationFailure do
221
+ @@db.eval("return sum(2,3);")
222
+ end
223
+ end
224
+
225
+ def test_eval
226
+ @@db.eval("db.system.save({_id:'hello', value: function() { print('hello'); } })")
227
+ assert_equal 'hello', @@db['system'].find_one['_id']
228
+ end
229
+
230
+ if @@version >= "1.3.5"
231
+ def test_db_stats
232
+ stats = @@db.stats
233
+ assert stats.has_key?('collections')
234
+ assert stats.has_key?('dataSize')
235
+ end
236
+ end
237
+
238
+ context "database profiling" do
239
+ setup do
240
+ @db = @@conn[MONGO_TEST_DB]
241
+ @coll = @db['test']
242
+ @coll.remove
243
+ @r1 = @coll.insert('a' => 1) # collection not created until it's used
244
+ end
245
+
246
+ should "set default profiling level" do
247
+ assert_equal :off, @db.profiling_level
248
+ end
249
+
250
+ should "change profiling level" do
251
+ @db.profiling_level = :slow_only
252
+ assert_equal :slow_only, @db.profiling_level
253
+ @db.profiling_level = :off
254
+ assert_equal :off, @db.profiling_level
255
+ @db.profiling_level = :all
256
+ assert_equal :all, @db.profiling_level
257
+ begin
258
+ @db.profiling_level = :medium
259
+ fail "shouldn't be able to do this"
260
+ rescue
261
+ end
262
+ end
263
+
264
+ should "return profiling info" do
265
+ @db.profiling_level = :all
266
+ @coll.find()
267
+ @db.profiling_level = :off
268
+
269
+ info = @db.profiling_info
270
+ assert_kind_of Array, info
271
+ assert info.length >= 1
272
+ first = info.first
273
+ assert_kind_of String, first['info']
274
+ assert_kind_of Time, first['ts']
275
+ assert_kind_of Numeric, first['millis']
276
+ end
277
+
278
+ should "validate collection" do
279
+ doc = @db.validate_collection(@coll.name)
280
+ assert_not_nil doc
281
+ result = doc['result']
282
+ assert_not_nil result
283
+ assert_match /firstExtent/, result
284
+ end
285
+
286
+ end
287
+ end
@@ -0,0 +1,244 @@
1
+ require './test/test_helper'
2
+ include Mongo
3
+
4
+ class GridFileSystemTest < Test::Unit::TestCase
5
+ context "GridFileSystem:" do
6
+ setup do
7
+ @con = standard_connection
8
+ @db = @con.db(MONGO_TEST_DB)
9
+ end
10
+
11
+ teardown do
12
+ @db.drop_collection('fs.files')
13
+ @db.drop_collection('fs.chunks')
14
+ end
15
+
16
+ context "When reading:" do
17
+ setup do
18
+ @chunks_data = "CHUNKS" * 50000
19
+ @grid = GridFileSystem.new(@db)
20
+ @grid.open('sample.file', 'w') do |f|
21
+ f.write @chunks_data
22
+ end
23
+
24
+ @grid = GridFileSystem.new(@db)
25
+ end
26
+
27
+ should "return existence of the file" do
28
+ file = @grid.exist?(:filename => 'sample.file')
29
+ assert_equal 'sample.file', file['filename']
30
+ end
31
+
32
+ should "return nil if the file doesn't exist" do
33
+ assert_nil @grid.exist?(:filename => 'foo.file')
34
+ end
35
+
36
+ should "read sample data" do
37
+ data = @grid.open('sample.file', 'r') { |f| f.read }
38
+ assert_equal data.length, @chunks_data.length
39
+ end
40
+
41
+ should "have a unique index on chunks" do
42
+ assert @db['fs.chunks'].index_information['files_id_1_n_1']['unique']
43
+ end
44
+
45
+ should "have an index on filename" do
46
+ assert @db['fs.files'].index_information['filename_1_uploadDate_-1']
47
+ end
48
+
49
+ should "return an empty string if length is zero" do
50
+ data = @grid.open('sample.file', 'r') { |f| f.read(0) }
51
+ assert_equal '', data
52
+ end
53
+
54
+ should "return the first n bytes" do
55
+ data = @grid.open('sample.file', 'r') {|f| f.read(288888) }
56
+ assert_equal 288888, data.length
57
+ assert_equal @chunks_data[0...288888], data
58
+ end
59
+
60
+ should "return the first n bytes even with an offset" do
61
+ data = @grid.open('sample.file', 'r') do |f|
62
+ f.seek(1000)
63
+ f.read(288888)
64
+ end
65
+ assert_equal 288888, data.length
66
+ assert_equal @chunks_data[1000...289888], data
67
+ end
68
+ end
69
+
70
+ context "When writing:" do
71
+ setup do
72
+ @data = "BYTES" * 50
73
+ @grid = GridFileSystem.new(@db)
74
+ @grid.open('sample', 'w') do |f|
75
+ f.write @data
76
+ end
77
+ end
78
+
79
+ should "read sample data" do
80
+ data = @grid.open('sample', 'r') { |f| f.read }
81
+ assert_equal data.length, @data.length
82
+ end
83
+
84
+ should "return the total number of bytes written" do
85
+ data = 'a' * 300000
86
+ assert_equal 300000, @grid.open('sample', 'w') {|f| f.write(data) }
87
+ end
88
+
89
+ should "more read sample data" do
90
+ data = @grid.open('sample', 'r') { |f| f.read }
91
+ assert_equal data.length, @data.length
92
+ end
93
+
94
+ should "raise exception if file not found" do
95
+ assert_raise GridFileNotFound do
96
+ @grid.open('io', 'r') { |f| f.write('hello') }
97
+ end
98
+ end
99
+
100
+ should "raise exception if not opened for write" do
101
+ assert_raise GridError do
102
+ @grid.open('sample', 'r') { |f| f.write('hello') }
103
+ end
104
+ end
105
+
106
+ context "and when overwriting the file" do
107
+ setup do
108
+ @old = @grid.open('sample', 'r')
109
+
110
+ @new_data = "DATA" * 10
111
+ sleep(2)
112
+ @grid.open('sample', 'w') do |f|
113
+ f.write @new_data
114
+ end
115
+
116
+ @new = @grid.open('sample', 'r')
117
+ end
118
+
119
+ should "have a newer upload date" do
120
+ assert @new.upload_date > @old.upload_date, "New data is not greater than old date."
121
+ end
122
+
123
+ should "have a different files_id" do
124
+ assert_not_equal @new.files_id, @old.files_id
125
+ end
126
+
127
+ should "contain the new data" do
128
+ assert_equal @new_data, @new.read, "Expected DATA"
129
+ end
130
+
131
+ context "and on a second overwrite" do
132
+ setup do
133
+ sleep(2)
134
+ @new_data = "NEW" * 1000
135
+ @grid.open('sample', 'w') do |f|
136
+ f.write @new_data
137
+ end
138
+
139
+ @ids = @db['fs.files'].find({'filename' => 'sample'}).map {|file| file['_id']}
140
+ end
141
+
142
+ should "write a third version of the file" do
143
+ assert_equal 3, @db['fs.files'].find({'filename' => 'sample'}).count
144
+ assert_equal 3, @db['fs.chunks'].find({'files_id' => {'$in' => @ids}}).count
145
+ end
146
+
147
+ should "remove all versions and their data on delete" do
148
+ @grid.delete('sample')
149
+ assert_equal 0, @db['fs.files'].find({'filename' => 'sample'}).count
150
+ assert_equal 0, @db['fs.chunks'].find({'files_id' => {'$in' => @ids}}).count
151
+ end
152
+
153
+ should "delete old versions on write with :delete_old is passed in" do
154
+ @grid.open('sample', 'w', :delete_old => true) do |f|
155
+ f.write @new_data
156
+ end
157
+ @new_ids = @db['fs.files'].find({'filename' => 'sample'}).map {|file| file['_id']}
158
+ assert_equal 1, @new_ids.length
159
+ id = @new_ids.first
160
+ assert !@ids.include?(id)
161
+ assert_equal 1, @db['fs.files'].find({'filename' => 'sample'}).count
162
+ assert_equal 1, @db['fs.chunks'].find({'files_id' => id}).count
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ context "When writing chunks:" do
169
+ setup do
170
+ data = "B" * 50000
171
+ @grid = GridFileSystem.new(@db)
172
+ @grid.open('sample', 'w', :chunk_size => 1000) do |f|
173
+ f.write data
174
+ end
175
+ end
176
+
177
+ should "write the correct number of chunks" do
178
+ file = @db['fs.files'].find_one({:filename => 'sample'})
179
+ chunks = @db['fs.chunks'].find({'files_id' => file['_id']}).to_a
180
+ assert_equal 50, chunks.length
181
+ end
182
+ end
183
+
184
+ context "Positioning:" do
185
+ setup do
186
+ data = 'hello, world' + '1' * 5000 + 'goodbye!' + '2' * 1000 + '!'
187
+ @grid = GridFileSystem.new(@db)
188
+ @grid.open('hello', 'w', :chunk_size => 1000) do |f|
189
+ f.write data
190
+ end
191
+ end
192
+
193
+ should "seek within chunks" do
194
+ @grid.open('hello', 'r') do |f|
195
+ f.seek(0)
196
+ assert_equal 'h', f.read(1)
197
+ f.seek(7)
198
+ assert_equal 'w', f.read(1)
199
+ f.seek(4)
200
+ assert_equal 'o', f.read(1)
201
+ f.seek(0)
202
+ f.seek(7, IO::SEEK_CUR)
203
+ assert_equal 'w', f.read(1)
204
+ f.seek(-2, IO::SEEK_CUR)
205
+ assert_equal ' ', f.read(1)
206
+ f.seek(-4, IO::SEEK_CUR)
207
+ assert_equal 'l', f.read(1)
208
+ f.seek(3, IO::SEEK_CUR)
209
+ assert_equal 'w', f.read(1)
210
+ end
211
+ end
212
+
213
+ should "seek between chunks" do
214
+ @grid.open('hello', 'r') do |f|
215
+ f.seek(1000)
216
+ assert_equal '11111', f.read(5)
217
+
218
+ f.seek(5009)
219
+ assert_equal '111goodbye!222', f.read(14)
220
+
221
+ f.seek(-1, IO::SEEK_END)
222
+ assert_equal '!', f.read(1)
223
+ f.seek(-6, IO::SEEK_END)
224
+ assert_equal '2', f.read(1)
225
+ end
226
+ end
227
+
228
+ should "tell the current position" do
229
+ @grid.open('hello', 'r') do |f|
230
+ assert_equal 0, f.tell
231
+
232
+ f.seek(999)
233
+ assert_equal 999, f.tell
234
+ end
235
+ end
236
+
237
+ should "seek only in read mode" do
238
+ assert_raise GridError do
239
+ @grid.open('hello', 'w') {|f| f.seek(0) }
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end