spqr 0.0.1

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/test/helper.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ require 'spqr/spqr'
7
+ require 'spqr/app'
8
+ require 'rhubarb/rhubarb'
9
+
10
+ class Test::Unit::TestCase
11
+ end
@@ -0,0 +1,608 @@
1
+ # Test cases for Rhubarb, which is is a simple persistence layer for
2
+ # Ruby objects and SQLite.
3
+ #
4
+ # Copyright (c) 2009 Red Hat, Inc.
5
+ #
6
+ # Author: William Benton (willb@redhat.com)
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+
14
+ require 'rubygems'
15
+ require 'rhubarb/rhubarb'
16
+ require 'test/unit'
17
+
18
+ class TestClass
19
+ include Rhubarb::Persisting
20
+ declare_column :foo, :integer
21
+ declare_column :bar, :string
22
+ end
23
+
24
+ class TestClass2
25
+ include Rhubarb::Persisting
26
+ declare_column :fred, :integer
27
+ declare_column :barney, :string
28
+ declare_index_on :fred
29
+ end
30
+
31
+ class TC3
32
+ include Rhubarb::Persisting
33
+ declare_column :ugh, :datetime
34
+ declare_column :yikes, :integer
35
+ declare_constraint :yikes_pos, check("yikes >= 0")
36
+ end
37
+
38
+ class TC4
39
+ include Rhubarb::Persisting
40
+ declare_column :t1, :integer, references(TestClass)
41
+ declare_column :t2, :integer, references(TestClass2)
42
+ declare_column :enabled, :boolean, :default, :true
43
+ end
44
+
45
+ class SelfRef
46
+ include Rhubarb::Persisting
47
+ declare_column :one, :integer, references(SelfRef)
48
+ end
49
+
50
+ class CustomQueryTable
51
+ include Rhubarb::Persisting
52
+ declare_column :one, :integer
53
+ declare_column :two, :integer
54
+ declare_query :ltcols, "one < two"
55
+ declare_query :ltvars, "one < ? and two < ?"
56
+ declare_custom_query :cltvars, "select * from __TABLE__ where one < ? and two < ?"
57
+ end
58
+
59
+ class ToRef
60
+ include Rhubarb::Persisting
61
+ declare_column :foo, :string
62
+ end
63
+
64
+ class FromRef
65
+ include Rhubarb::Persisting
66
+ declare_column :t, :integer, references(ToRef, :on_delete=>:cascade)
67
+ end
68
+
69
+ class FreshTestTable
70
+ include Rhubarb::Persisting
71
+ declare_column :fee, :integer
72
+ declare_column :fie, :integer
73
+ declare_column :foe, :integer
74
+ declare_column :fum, :integer
75
+ end
76
+
77
+ class BackendBasicTests < Test::Unit::TestCase
78
+ def setup
79
+ Rhubarb::Persistence::open(":memory:")
80
+ klasses = []
81
+ klasses << TestClass
82
+ klasses << TestClass2
83
+ klasses << TC3
84
+ klasses << TC4
85
+ klasses << SelfRef
86
+ klasses << CustomQueryTable
87
+ klasses << ToRef
88
+ klasses << FromRef
89
+ klasses << FreshTestTable
90
+
91
+ klasses.each { |klass| klass.create_table }
92
+
93
+ @flist = []
94
+ end
95
+
96
+ def teardown
97
+ Rhubarb::Persistence::close()
98
+ end
99
+
100
+ def test_persistence_setup
101
+ assert Rhubarb::Persistence::db.type_translation, "type translation not enabled for db"
102
+ assert Rhubarb::Persistence::db.results_as_hash, "rows-as-hashes not enabled for db"
103
+ end
104
+
105
+ def test_reference_ctor_klass
106
+ r = Rhubarb::Reference.new(TestClass)
107
+ assert(r.referent == TestClass, "Referent of managed reference instance incorrect")
108
+ assert(r.column == "row_id", "Column of managed reference instance incorrect")
109
+ assert(r.to_s == "references TestClass(row_id)", "string representation of managed reference instance incorrect")
110
+ assert(r.managed_ref?, "managed reference should return true for managed_ref?")
111
+ end
112
+
113
+ def test_reference_ctor_string
114
+ r = Rhubarb::Reference.new("TestClass")
115
+ assert(r.referent == "TestClass", "Referent of string-backed reference instance incorrect")
116
+ assert(r.column == "row_id", "Column of string-backed reference instance incorrect")
117
+ assert(r.to_s == "references TestClass(row_id)", "string representation of string-backed reference instance incorrect")
118
+ assert(r.managed_ref? == false, "unmanaged reference should return false for managed_ref?")
119
+ end
120
+
121
+ def test_instance_methods
122
+ ["foo", "bar"].each do |prefix|
123
+ ["#{prefix}", "#{prefix}="].each do |m|
124
+ assert TestClass.instance_methods.include?(m), "#{m} method not declared in TestClass"
125
+ end
126
+ end
127
+ end
128
+
129
+ def test_instance_methods2
130
+ ["fred", "barney"].each do |prefix|
131
+ ["#{prefix}", "#{prefix}="].each do |m|
132
+ assert TestClass2.instance_methods.include?(m), "#{m} method not declared in TestClass2"
133
+ end
134
+ end
135
+ end
136
+
137
+ def test_instance_methods_neg
138
+ ["fred", "barney"].each do |prefix|
139
+ ["#{prefix}", "#{prefix}="].each do |m|
140
+ bogus_include = TestClass.instance_methods.include? m
141
+ assert(bogus_include == false, "#{m} method declared in TestClass; shouldn't be")
142
+ end
143
+ end
144
+ end
145
+
146
+ def test_instance_methods_dont_include_class_methods
147
+ ["foo", "bar"].each do |prefix|
148
+ ["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
149
+ bogus_include = TestClass.instance_methods.include? m
150
+ assert(bogus_include == false, "#{m} method declared in TestClass; shouldn't be")
151
+ end
152
+ end
153
+ end
154
+
155
+ def test_class_methods
156
+ ["foo", "bar"].each do |prefix|
157
+ ["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
158
+ klass = class << TestClass; self end
159
+ assert klass.instance_methods.include?(m), "#{m} method not declared in TestClass' eigenclass"
160
+ end
161
+ end
162
+ end
163
+
164
+ def test_class_methods2
165
+ ["fred", "barney"].each do |prefix|
166
+ ["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
167
+ klass = class << TestClass2; self end
168
+ assert klass.instance_methods.include?(m), "#{m} method not declared in TestClass2's eigenclass"
169
+ end
170
+ end
171
+ end
172
+
173
+ def test_table_class_methods_neg
174
+ ["foo", "bar", "fred", "barney"].each do |prefix|
175
+ ["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
176
+ klass = Class.new
177
+
178
+ klass.class_eval do
179
+ include Rhubarb::Persisting
180
+ end
181
+
182
+ bogus_include = klass.instance_methods.include?(m)
183
+ assert(bogus_include == false, "#{m} method declared in eigenclass of class including Rhubarb::Persisting; shouldn't be")
184
+ end
185
+ end
186
+ end
187
+
188
+ def test_class_methods_neg
189
+ ["fred", "barney"].each do |prefix|
190
+ ["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
191
+ klass = class << TestClass; self end
192
+ bogus_include = klass.instance_methods.include?(m)
193
+ assert(bogus_include == false, "#{m} method declared in TestClass' eigenclass; shouldn't be")
194
+ end
195
+ end
196
+ end
197
+
198
+ def test_column_size
199
+ assert(TestClass.columns.size == 5, "TestClass has wrong number of columns")
200
+ end
201
+
202
+ def test_tc2_column_size
203
+ assert(TestClass2.columns.size == 5, "TestClass2 has wrong number of columns")
204
+ end
205
+
206
+ def test_table_column_size
207
+ klass = Class.new
208
+ klass.class_eval do
209
+ include Rhubarb::Persisting
210
+ end
211
+ if klass.respond_to? :columns
212
+ assert(Frotz.columns.size == 0, "A persisting class with no declared columns has the wrong number of columns")
213
+ end
214
+ end
215
+
216
+ def test_constraints_size
217
+ k = Class.new
218
+
219
+ k.class_eval do
220
+ include Rhubarb::Persisting
221
+ end
222
+
223
+ {k => 0, TestClass => 0, TestClass2 => 0, TC3 => 1}.each do |klass, cts|
224
+ if klass.respond_to? :constraints
225
+ assert(klass.constraints.size == cts, "#{klass} has wrong number of constraints")
226
+ end
227
+ end
228
+ end
229
+
230
+ def test_cols_and_constraints_understood
231
+ [TestClass, TestClass2, TC3, TC4].each do |klass|
232
+ assert(klass.respond_to?(:constraints), "#{klass} should have accessor for constraints")
233
+ assert(klass.respond_to?(:columns), "#{klass} should have accessor for columns")
234
+ end
235
+ end
236
+
237
+ def test_column_contents
238
+ [:row_id, :foo, :bar].each do |col|
239
+ assert(TestClass.columns.map{|c| c.name}.include?(col), "TestClass doesn't contain column #{col}")
240
+ end
241
+ end
242
+
243
+ def test_create_proper_type
244
+ tc = TestClass.create(:foo => 1, :bar => "argh")
245
+ assert(tc.class == TestClass, "TestClass.create should return an instance of TestClass")
246
+ end
247
+
248
+ def test_create_multiples
249
+ tc_list = [nil]
250
+ 1.upto(9) do |num|
251
+ tc_list.push TestClass.create(:foo => num, :bar => "argh#{num}")
252
+ end
253
+
254
+ 1.upto(9) do |num|
255
+ assert(tc_list[num].foo == num, "multiple TestClass.create invocations should return records with proper foo values")
256
+ assert(tc_list[num].bar == "argh#{num}", "multiple TestClass.create invocations should return records with proper bar values")
257
+
258
+ tmp = TestClass.find(num)
259
+
260
+ assert(tmp.foo == num, "multiple TestClass.create invocations should add records with proper foo values to the db")
261
+ assert(tmp.bar == "argh#{num}", "multiple TestClass.create invocations should add records with proper bar values to the db")
262
+ end
263
+ end
264
+
265
+ def test_delete
266
+ range = []
267
+ 1.upto(9) do |num|
268
+ range << num
269
+ TestClass.create(:foo => num, :bar => "argh#{num}")
270
+ end
271
+
272
+ assert(TestClass.count == range.size, "correct number of rows inserted prior to delete")
273
+
274
+ TestClass.find(2).delete
275
+
276
+ assert(TestClass.count == range.size - 1, "correct number of rows inserted after delete")
277
+ end
278
+
279
+ def test_delete_2
280
+ TestClass.create(:foo => 42, :bar => "Wait, what was the question?")
281
+ tc1 = TestClass.find_first_by_foo(42)
282
+ tc2 = TestClass.find_first_by_foo(42)
283
+
284
+ [tc1,tc2].each do |obj|
285
+ assert obj
286
+ assert_kind_of TestClass, obj
287
+ assert_equal false, obj.instance_eval {needs_refresh?}
288
+ end
289
+
290
+ [:foo, :bar, :row_id].each do |msg|
291
+ assert_equal tc1.send(msg), tc2.send(msg)
292
+ end
293
+
294
+ tc1.delete
295
+
296
+ tc3 = TestClass.find_by_foo(42)
297
+ assert_equal [], tc3
298
+
299
+ tc3 = TestClass.find_first_by_foo(42)
300
+ assert_equal nil, tc3
301
+
302
+ [tc1, tc2].each do |obj|
303
+ assert obj.deleted?
304
+ [:foo, :bar, :row_id].each do |msg|
305
+ assert_equal nil, obj.send(msg)
306
+ end
307
+ end
308
+ end
309
+
310
+ def test_count_base
311
+ assert(TestClass.count == 0, "a new table should have no rows")
312
+ end
313
+
314
+ def test_count_inc
315
+ 1.upto(9) do |num|
316
+ TestClass.create(:foo => num, :bar => "argh#{num}")
317
+ assert(TestClass.count == num, "table row count should increment after each row create")
318
+ end
319
+ end
320
+
321
+ def test_create_proper_values
322
+ vals = {:foo => 1, :bar => "argh"}
323
+ tc = TestClass.create(vals)
324
+ assert(tc.foo == 1, "tc.foo (newly-created) should have the value 1")
325
+ assert(tc.bar == "argh", "tc.bar (newly-created) should have the value \"argh\"")
326
+ end
327
+
328
+ def test_create_and_find_by_id
329
+ vals = {:foo => 2, :bar => "argh"}
330
+ TestClass.create(vals)
331
+
332
+ tc = TestClass.find(1)
333
+ assert(tc.foo == 2, "tc.foo (found by id) should have the value 2")
334
+ assert(tc.bar == "argh", "tc.bar (found by id) should have the value \"argh\"")
335
+ end
336
+
337
+ def test_find_by_id_bogus
338
+ tc = TestClass.find(1)
339
+ assert(tc == nil, "TestClass table should be empty")
340
+ end
341
+
342
+ def test_create_and_find_by_foo
343
+ vals = {:foo => 2, :bar => "argh"}
344
+ TestClass.create(vals)
345
+
346
+ result = TestClass.find_by_foo(2)
347
+ tc = result[0]
348
+ assert(result.size == 1, "TestClass.find_by_foo(2) should return exactly one result")
349
+ assert(tc.foo == 2, "tc.foo (found by foo) should have the value 2")
350
+ assert(tc.bar == "argh", "tc.bar (found by foo) should have the value \"argh\"")
351
+ end
352
+
353
+ def test_create_and_find_first_by_foo
354
+ vals = {:foo => 2, :bar => "argh"}
355
+ TestClass.create(vals)
356
+
357
+ tc = (TestClass.find_first_by_foo(2))
358
+ assert(tc.foo == 2, "tc.foo (found by foo) should have the value 2")
359
+ assert(tc.bar == "argh", "tc.bar (found by foo) should have the value \"argh\"")
360
+ end
361
+
362
+ def test_create_and_find_by_bar
363
+ vals = {:foo => 2, :bar => "argh"}
364
+ TestClass.create(vals)
365
+ result = TestClass.find_by_bar("argh")
366
+ tc = result[0]
367
+ assert(result.size == 1, "TestClass.find_by_bar(\"argh\") should return exactly one result")
368
+ assert(tc.foo == 2, "tc.foo (found by bar) should have the value 2")
369
+ assert(tc.bar == "argh", "tc.bar (found by bar) should have the value \"argh\"")
370
+ end
371
+
372
+ def test_create_and_find_first_by_bar
373
+ vals = {:foo => 2, :bar => "argh"}
374
+ TestClass.create(vals)
375
+
376
+ tc = (TestClass.find_first_by_bar("argh"))
377
+ assert(tc.foo == 2, "tc.foo (found by bar) should have the value 2")
378
+ assert(tc.bar == "argh", "tc.bar (found by bar) should have the value \"argh\"")
379
+ end
380
+
381
+ def test_create_and_update_modifies_object
382
+ vals = {:foo => 1, :bar => "argh"}
383
+ TestClass.create(vals)
384
+
385
+ tc = TestClass.find(1)
386
+ tc.foo = 2
387
+ assert("#{tc.foo}" == "2", "tc.foo should have the value 2 after modifying object")
388
+ end
389
+
390
+ def test_create_and_update_modifies_db
391
+ vals = {:foo => 1, :bar => "argh"}
392
+ TestClass.create(vals)
393
+
394
+ tc = TestClass.find(1)
395
+ tc.foo = 2
396
+
397
+ tc_fresh = TestClass.find(1)
398
+ assert(tc_fresh.foo == 2, "foo value in first row of db should have the value 2 after modifying tc object")
399
+ end
400
+
401
+ def test_create_and_update_freshen
402
+ vals = {:foo => 1, :bar => "argh"}
403
+ TestClass.create(vals)
404
+
405
+ tc_fresh = TestClass.find(1)
406
+ tc = TestClass.find(1)
407
+
408
+ tc.foo = 2
409
+
410
+ assert(tc_fresh.foo == 2, "object backed by db row isn't freshened")
411
+ end
412
+
413
+ def test_reference_tables
414
+ assert(TC4.refs.size == 2, "TC4 should have 2 refs, instead has #{TC4.refs.size}")
415
+ end
416
+
417
+ def test_reference_classes
418
+ t_vals = []
419
+ t2_vals = []
420
+
421
+ 1.upto(9) do |n|
422
+ t_vals.push({:foo => n, :bar => "item-#{n}"})
423
+ TestClass.create t_vals[-1]
424
+ end
425
+
426
+ 9.downto(1) do |n|
427
+ t2_vals.push({:fred => n, :barney => "barney #{n}"})
428
+ TestClass2.create t2_vals[-1]
429
+ end
430
+
431
+ 1.upto(9) do |n|
432
+ m = 10-n
433
+ k = TC4.create(:t1 => n, :t2 => m)
434
+ assert(k.t1.class == TestClass, "k.t1.class is #{k.t1.class}; should be TestClass")
435
+ assert(k.t2.class == TestClass2, "k.t2.class is #{k.t2.class}; should be TestClass2")
436
+ assert(k.enabled)
437
+ k.enabled = false
438
+ assert(k.enabled==false)
439
+ end
440
+ end
441
+
442
+ def test_references_simple
443
+ t_vals = []
444
+ t2_vals = []
445
+
446
+ 1.upto(9) do |n|
447
+ t_vals.push({:foo => n, :bar => "item-#{n}"})
448
+ TestClass.create t_vals[-1]
449
+ end
450
+
451
+ 9.downto(1) do |n|
452
+ t2_vals.push({:fred => n, :barney => "barney #{n}"})
453
+ TestClass2.create t2_vals[-1]
454
+ end
455
+
456
+ 1.upto(9) do |n|
457
+ k = TC4.create(:t1 => n, :t2 => (10 - n))
458
+ assert(k.t1.foo == k.t2.fred, "references don't work")
459
+ end
460
+ end
461
+
462
+ def test_references_sameclass
463
+ SelfRef.create :one => nil
464
+ 1.upto(3) do |num|
465
+ SelfRef.create :one => num
466
+ end
467
+ 4.downto(2) do |num|
468
+ sr = SelfRef.find num
469
+ assert(sr.one.class == SelfRef, "SelfRef with row ID #{num} should have a one field of type SelfRef; is #{sr.one.class} instead")
470
+ assert(sr.one.row_id == sr.row_id - 1, "SelfRef with row ID #{num} should have a one field with a row id of #{sr.row_id - 1}; is #{sr.one.row_id} instead")
471
+ end
472
+ end
473
+
474
+ def test_references_circular_id
475
+ sr = SelfRef.create :one => nil
476
+ sr.one = sr.row_id
477
+ assert(sr == sr.one, "self-referential rows should work; instead #{sr} isn't the same as #{sr.one}")
478
+ end
479
+
480
+ def test_references_circular_obj
481
+ sr = SelfRef.create :one => nil
482
+ sr.one = sr
483
+ assert(sr == sr.one, "self-referential rows should work; instead #{sr} isn't the same as #{sr.one}")
484
+ end
485
+
486
+ def test_referential_integrity
487
+ assert_raise SQLite3::SQLException do
488
+ FromRef.create(:t => 42)
489
+ end
490
+
491
+ assert_nothing_thrown do
492
+ 1.upto(20) do |x|
493
+ ToRef.create(:foo => "#{x}")
494
+ FromRef.create(:t => x)
495
+ assert_equal ToRef.count, FromRef.count
496
+ end
497
+ end
498
+
499
+ 20.downto(1) do |x|
500
+ ct = ToRef.count
501
+ tr = ToRef.find(x)
502
+ tr.delete
503
+ assert_equal ToRef.count, ct - 1
504
+ assert_equal ToRef.count, FromRef.count
505
+ end
506
+ end
507
+
508
+ def test_custom_query
509
+ colresult = 0
510
+ varresult = 0
511
+
512
+ 1.upto(20) do |i|
513
+ 1.upto(20) do |j|
514
+ CustomQueryTable.create(:one => i, :two => j)
515
+ colresult = colresult.succ if i < j
516
+ varresult = varresult.succ if i < 5 && j < 7
517
+ end
518
+ end
519
+
520
+ f = CustomQueryTable.ltcols
521
+ assert(f.size() == colresult, "f.size() should equal colresult, but #{f.size()} != #{colresult}")
522
+ f.each {|r| assert(r.one < r.two, "#{r.one}, #{r.two} should only be in ltcols custom query if #{r.one} < #{r.two}") }
523
+
524
+ f = CustomQueryTable.ltvars 5, 7
525
+ f2 = CustomQueryTable.cltvars 5, 7
526
+
527
+ [f,f2].each do |obj|
528
+ assert(obj.size() == varresult, "query result size should equal varresult, but #{obj.size()} != #{varresult}")
529
+ obj.each {|r| assert(r.one < 5 && r.two < 7, "#{r.one}, #{r.two} should only be in ltvars/cltvars custom query if #{r.one} < 5 && #{r.two} < 7") }
530
+ end
531
+
532
+ end
533
+
534
+ def freshness_query_fixture
535
+ @flist = []
536
+
537
+ 0.upto(99) do |x|
538
+ @flist << FreshTestTable.create(:fee=>x, :fie=>(x%7), :foe=>(x%11), :fum=>(x%13))
539
+ end
540
+ end
541
+
542
+ def test_freshness_query_basic
543
+ freshness_query_fixture
544
+ # basic test
545
+ basic = FreshTestTable.find_freshest(:group_by=>[:fee])
546
+
547
+ assert_equal(@flist.size, basic.size)
548
+ 0.upto(99) do |x|
549
+ [:fee,:fie,:foe,:fum,:created,:updated,:row_id].each do |msg|
550
+ assert_equal(@flist[x].send(msg), basic[x].send(msg))
551
+ end
552
+ end
553
+ end
554
+
555
+ def test_freshness_query_basic_restricted
556
+ freshness_query_fixture
557
+ # basic test
558
+
559
+ basic = FreshTestTable.find_freshest(:group_by=>[:fee], :version=>@flist[30].created, :debug=>true)
560
+
561
+ assert_equal(31, basic.size)
562
+ 0.upto(30) do |x|
563
+ [:fee,:fie,:foe,:fum,:created,:updated,:row_id].each do |msg|
564
+ assert_equal(@flist[x].send(msg), basic[x].send(msg))
565
+ end
566
+ end
567
+ end
568
+
569
+ def test_freshness_query_basic_select
570
+ freshness_query_fixture
571
+ # basic test
572
+
573
+ basic = FreshTestTable.find_freshest(:group_by=>[:fee], :select_by=>{:fie=>0}, :debug=>true)
574
+
575
+ expected_ct = 99/7 + 1;
576
+
577
+ assert_equal(expected_ct, basic.size)
578
+
579
+ 0.upto(expected_ct - 1) do |x|
580
+ [:fee,:fie,:foe,:fum,:created,:updated,:row_id].each do |msg|
581
+ assert_equal(@flist[x*7].send(msg), basic[x].send(msg))
582
+ end
583
+ end
584
+ end
585
+
586
+ def test_freshness_query_group_single
587
+ freshness_query_fixture
588
+ # more basic tests
589
+ pairs = {:fie=>7,:foe=>11,:fum=>13}
590
+ pairs.each do |col,ct|
591
+ basic = FreshTestTable.find_freshest(:group_by=>[col])
592
+ assert_equal(ct,basic.size)
593
+
594
+ expected_objs = {}
595
+
596
+ 99.downto(99-ct+1) do |x|
597
+ expected_objs[x%ct] = @flist[x]
598
+ end
599
+
600
+ basic.each do |row|
601
+ res = expected_objs[row.send(col)]
602
+ [:fee,:fie,:foe,:fum,:created,:updated,:row_id].each do |msg|
603
+ assert_equal(res.send(msg), row.send(msg))
604
+ end
605
+ end
606
+ end
607
+ end
608
+ end