aliyun-oss-ex 0.7.0.1402831795

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +19 -0
  3. data/INSTALL +35 -0
  4. data/README +443 -0
  5. data/Rakefile +334 -0
  6. data/bin/oss +6 -0
  7. data/bin/setup.rb +11 -0
  8. data/lib/aliyun/oss.rb +55 -0
  9. data/lib/aliyun/oss/acl.rb +132 -0
  10. data/lib/aliyun/oss/authentication.rb +222 -0
  11. data/lib/aliyun/oss/base.rb +241 -0
  12. data/lib/aliyun/oss/bucket.rb +320 -0
  13. data/lib/aliyun/oss/connection.rb +279 -0
  14. data/lib/aliyun/oss/error.rb +70 -0
  15. data/lib/aliyun/oss/exceptions.rb +134 -0
  16. data/lib/aliyun/oss/extensions.rb +405 -0
  17. data/lib/aliyun/oss/logging.rb +304 -0
  18. data/lib/aliyun/oss/object.rb +612 -0
  19. data/lib/aliyun/oss/owner.rb +45 -0
  20. data/lib/aliyun/oss/parsing.rb +100 -0
  21. data/lib/aliyun/oss/response.rb +181 -0
  22. data/lib/aliyun/oss/service.rb +52 -0
  23. data/lib/aliyun/oss/version.rb +14 -0
  24. data/support/faster-xml-simple/lib/faster_xml_simple.rb +188 -0
  25. data/support/faster-xml-simple/test/regression_test.rb +48 -0
  26. data/support/faster-xml-simple/test/test_helper.rb +18 -0
  27. data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +47 -0
  28. data/support/rdoc/code_info.rb +212 -0
  29. data/test/acl_test.rb +70 -0
  30. data/test/authentication_test.rb +114 -0
  31. data/test/base_test.rb +137 -0
  32. data/test/bucket_test.rb +75 -0
  33. data/test/connection_test.rb +218 -0
  34. data/test/error_test.rb +71 -0
  35. data/test/extensions_test.rb +346 -0
  36. data/test/fixtures.rb +90 -0
  37. data/test/fixtures/buckets.yml +133 -0
  38. data/test/fixtures/errors.yml +34 -0
  39. data/test/fixtures/headers.yml +3 -0
  40. data/test/fixtures/logging.yml +15 -0
  41. data/test/fixtures/loglines.yml +5 -0
  42. data/test/fixtures/logs.yml +7 -0
  43. data/test/fixtures/policies.yml +16 -0
  44. data/test/logging_test.rb +90 -0
  45. data/test/mocks/fake_response.rb +27 -0
  46. data/test/object_test.rb +221 -0
  47. data/test/parsing_test.rb +67 -0
  48. data/test/remote/acl_test.rb +28 -0
  49. data/test/remote/bucket_test.rb +147 -0
  50. data/test/remote/logging_test.rb +86 -0
  51. data/test/remote/object_test.rb +350 -0
  52. data/test/remote/test_file.data +0 -0
  53. data/test/remote/test_helper.rb +34 -0
  54. data/test/response_test.rb +69 -0
  55. data/test/service_test.rb +24 -0
  56. data/test/test_helper.rb +110 -0
  57. metadata +177 -0
@@ -0,0 +1,346 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require File.dirname(__FILE__) + '/test_helper'
3
+
4
+ class HashExtensionsTest < Test::Unit::TestCase
5
+ def test_to_query_string
6
+ # Because hashes aren't ordered, I'm mostly testing against hashes with just one key
7
+ symbol_keys = {:one => 1}
8
+ string_keys = {'one' => 1}
9
+ expected = '?one=1'
10
+ [symbol_keys, string_keys].each do |hash|
11
+ assert_equal expected, hash.to_query_string
12
+ end
13
+ end
14
+
15
+ def test_empty_hash_returns_no_query_string
16
+ assert_equal '', {}.to_query_string
17
+ end
18
+
19
+ def test_include_question_mark
20
+ hash = {:one => 1}
21
+ assert_equal '?one=1', hash.to_query_string
22
+ assert_equal 'one=1', hash.to_query_string(false)
23
+ end
24
+
25
+ def test_elements_joined_by_ampersand
26
+ hash = {:one => 1, :two => 2}
27
+ qs = hash.to_query_string
28
+ assert qs['one=1&two=2'] || qs['two=2&one=1']
29
+ end
30
+
31
+ def test_normalized_options
32
+ expectations = [
33
+ [{:foo_bar => 1}, {'foo-bar' => '1'}],
34
+ [{'foo_bar' => 1}, {'foo-bar' => '1'}],
35
+ [{'foo-bar' => 1}, {'foo-bar' => '1'}],
36
+ [{}, {}]
37
+ ]
38
+
39
+ expectations.each do |(before, after)|
40
+ assert_equal after, before.to_normalized_options
41
+ end
42
+ end
43
+ end
44
+
45
+ class StringExtensionsTest < Test::Unit::TestCase
46
+ def test_previous
47
+ expectations = {'abc' => 'abb', '123' => '122', '1' => '0'}
48
+ expectations.each do |before, after|
49
+ assert_equal after, before.previous
50
+ end
51
+ end
52
+
53
+ def test_to_header
54
+ transformations = {
55
+ 'foo' => 'foo',
56
+ :foo => 'foo',
57
+ 'foo-bar' => 'foo-bar',
58
+ 'foo_bar' => 'foo-bar',
59
+ :foo_bar => 'foo-bar',
60
+ 'Foo-Bar' => 'foo-bar',
61
+ 'Foo_Bar' => 'foo-bar'
62
+ }
63
+
64
+ transformations.each do |before, after|
65
+ assert_equal after, before.to_header
66
+ end
67
+ end
68
+
69
+ def test_valid_utf8?
70
+ assert !"318597/620065/GTL_75\24300_A600_A610.zip".valid_utf8?
71
+ assert "318597/620065/GTL_75£00_A600_A610.zip".valid_utf8?
72
+ end
73
+
74
+ def test_remove_extended
75
+ assert "318597/620065/GTL_75\24300_A600_A610.zip".remove_extended.valid_utf8?
76
+ assert "318597/620065/GTL_75£00_A600_A610.zip".remove_extended.valid_utf8?
77
+ end
78
+
79
+ def test_tap
80
+ assert_equal("http://google.com/foo/", "http://google.com".tap {|url| url << "/foo/" })
81
+ end
82
+
83
+ end
84
+
85
+ class CoercibleStringTest < Test::Unit::TestCase
86
+
87
+ def test_coerce
88
+ coercions = [
89
+ ['1', 1],
90
+ ['false', false],
91
+ ['true', true],
92
+ ['2006-10-29T23:14:47.000Z', Time.parse('2006-10-29T23:14:47.000Z')],
93
+ ['Hello!', 'Hello!'],
94
+ ['false23', 'false23'],
95
+ ['03 1-2-3-Apple-Tree.mp3', '03 1-2-3-Apple-Tree.mp3'],
96
+ ['0815', '0815'] # This number isn't coerced because the leading zero would be lost
97
+ ]
98
+
99
+ coercions.each do |before, after|
100
+ assert_nothing_raised do
101
+ assert_equal after, CoercibleString.coerce(before)
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ class KerneltExtensionsTest < Test::Unit::TestCase
108
+ class Foo
109
+ def foo
110
+ __method__
111
+ end
112
+
113
+ def bar
114
+ foo
115
+ end
116
+
117
+ def baz
118
+ bar
119
+ end
120
+ end
121
+
122
+ class Bar
123
+ def foo
124
+ calling_method
125
+ end
126
+
127
+ def bar
128
+ calling_method
129
+ end
130
+
131
+ def calling_method
132
+ __method__(1)
133
+ end
134
+ end
135
+
136
+ def test___method___works_regardless_of_nesting
137
+ f = Foo.new
138
+ [:foo, :bar, :baz].each do |method|
139
+ assert_equal 'foo', f.send(method)
140
+ end
141
+ end
142
+
143
+ def test___method___depth
144
+ b = Bar.new
145
+ assert_equal 'foo', b.foo
146
+ assert_equal 'bar', b.bar
147
+ end
148
+ end if RUBY_VERSION <= '1.8.7'
149
+
150
+ class ModuleExtensionsTest < Test::Unit::TestCase
151
+ class Foo
152
+ def foo(reload = false)
153
+ expirable_memoize(reload) do
154
+ Time.now
155
+ end
156
+ end
157
+
158
+ def bar(reload = false)
159
+ expirable_memoize(reload, :baz) do
160
+ Time.now
161
+ end
162
+ end
163
+
164
+ def quux
165
+ Time.now
166
+ end
167
+ memoized :quux
168
+ end
169
+
170
+ def setup
171
+ @instance = Foo.new
172
+ end
173
+
174
+ def test_memoize
175
+ assert !instance_variables_of(@instance).include?('@foo')
176
+ cached_result = @instance.foo
177
+ assert_equal cached_result, @instance.foo
178
+ assert instance_variables_of(@instance).include?('@foo')
179
+ assert_equal cached_result, @instance.send(:instance_variable_get, :@foo)
180
+ assert_not_equal cached_result, new_cache = @instance.foo(:reload)
181
+ assert_equal new_cache, @instance.foo
182
+ assert_equal new_cache, @instance.send(:instance_variable_get, :@foo)
183
+ end
184
+
185
+ def test_customizing_memoize_storage
186
+ assert !instance_variables_of(@instance).include?('@bar')
187
+ assert !instance_variables_of(@instance).include?('@baz')
188
+ cached_result = @instance.bar
189
+ assert !instance_variables_of(@instance).include?('@bar')
190
+ assert instance_variables_of(@instance).include?('@baz')
191
+ assert_equal cached_result, @instance.bar
192
+ assert_equal cached_result, @instance.send(:instance_variable_get, :@baz)
193
+ assert_nil @instance.send(:instance_variable_get, :@bar)
194
+ end
195
+
196
+ def test_memoized
197
+ assert !instance_variables_of(@instance).include?('@quux')
198
+ cached_result = @instance.quux
199
+ assert_equal cached_result, @instance.quux
200
+ assert instance_variables_of(@instance).include?('@quux')
201
+ assert_equal cached_result, @instance.send(:instance_variable_get, :@quux)
202
+ assert_not_equal cached_result, new_cache = @instance.quux(:reload)
203
+ assert_equal new_cache, @instance.quux
204
+ assert_equal new_cache, @instance.send(:instance_variable_get, :@quux)
205
+ end
206
+
207
+ def test_constant_setting
208
+ some_module = Module.new
209
+ assert !some_module.const_defined?(:FOO)
210
+ assert_nothing_raised do
211
+ some_module.constant :FOO, 'bar'
212
+ end
213
+
214
+ assert some_module.const_defined?(:FOO)
215
+ assert_nothing_raised do
216
+ some_module::FOO
217
+ some_module.foo
218
+ end
219
+ assert_equal 'bar', some_module::FOO
220
+ assert_equal 'bar', some_module.foo
221
+
222
+ assert_nothing_raised do
223
+ some_module.constant :FOO, 'baz'
224
+ end
225
+
226
+ assert_equal 'bar', some_module::FOO
227
+ assert_equal 'bar', some_module.foo
228
+ end
229
+
230
+ private
231
+ # For 1.9 compatibility
232
+ def instance_variables_of(object)
233
+ object.instance_variables.map do |instance_variable|
234
+ instance_variable.to_s
235
+ end
236
+ end
237
+
238
+ end
239
+
240
+ class AttributeProxyTest < Test::Unit::TestCase
241
+ class BlindProxyUsingDefaultAttributesHash
242
+ include SelectiveAttributeProxy
243
+ proxy_to :exlusively => false
244
+ end
245
+
246
+ class BlindProxyUsingCustomAttributeHash
247
+ include SelectiveAttributeProxy
248
+ proxy_to :settings
249
+ end
250
+
251
+ class ProxyUsingPassedInAttributeHash
252
+ include SelectiveAttributeProxy
253
+
254
+ def initialize(attributes = {})
255
+ @attributes = attributes
256
+ end
257
+ end
258
+
259
+ class RestrictedProxy
260
+ include SelectiveAttributeProxy
261
+
262
+ private
263
+ def proxiable_attribute?(name)
264
+ %w(foo bar baz).include?(name)
265
+ end
266
+ end
267
+
268
+ class NonExclusiveProxy
269
+ include SelectiveAttributeProxy
270
+ proxy_to :settings, :exclusively => false
271
+ end
272
+
273
+ def test_using_all_defaults
274
+ b = BlindProxyUsingDefaultAttributesHash.new
275
+ assert_nothing_raised do
276
+ b.foo = 'bar'
277
+ end
278
+
279
+ assert_nothing_raised do
280
+ b.foo
281
+ end
282
+
283
+ assert_equal 'bar', b.foo
284
+ end
285
+
286
+ def test_storage_is_autovivified
287
+ b = BlindProxyUsingDefaultAttributesHash.new
288
+ assert_nothing_raised do
289
+ b.send(:attributes)['foo'] = 'bar'
290
+ end
291
+
292
+ assert_nothing_raised do
293
+ b.foo
294
+ end
295
+
296
+ assert_equal 'bar', b.foo
297
+ end
298
+
299
+ def test_limiting_which_attributes_are_proxiable
300
+ r = RestrictedProxy.new
301
+ assert_nothing_raised do
302
+ r.foo = 'bar'
303
+ end
304
+
305
+ assert_nothing_raised do
306
+ r.foo
307
+ end
308
+
309
+ assert_equal 'bar', r.foo
310
+
311
+ assert_raises(NoMethodError) do
312
+ r.quux = 'foo'
313
+ end
314
+
315
+ assert_raises(NoMethodError) do
316
+ r.quux
317
+ end
318
+ end
319
+
320
+ def test_proxying_is_exclusive_by_default
321
+ p = ProxyUsingPassedInAttributeHash.new('foo' => 'bar')
322
+ assert_nothing_raised do
323
+ p.foo
324
+ p.foo = 'baz'
325
+ end
326
+
327
+ assert_equal 'baz', p.foo
328
+
329
+ assert_raises(NoMethodError) do
330
+ p.quux
331
+ end
332
+ end
333
+
334
+ def test_setting_the_proxy_as_non_exclusive
335
+ n = NonExclusiveProxy.new
336
+ assert_nothing_raised do
337
+ n.foo = 'baz'
338
+ end
339
+
340
+ assert_nothing_raised do
341
+ n.foo
342
+ end
343
+
344
+ assert_equal 'baz', n.foo
345
+ end
346
+ end
@@ -0,0 +1,90 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'yaml'
3
+
4
+ module Aliyun
5
+ module OSS
6
+ # When this file is loaded, for each fixture file, a module is created within the Fixtures module
7
+ # with the same name as the fixture file. For each fixture in that fixture file, a singleton method is
8
+ # added to the module with the name of the given fixture, returning the value of the fixture.
9
+ #
10
+ # For example:
11
+ #
12
+ # A fixture in <tt>buckets.yml</tt> named <tt>empty_bucket_list</tt> with value <tt><foo>hi!</foo></tt>
13
+ # would be made available like so:
14
+ #
15
+ # Fixtures::Buckets.empty_bucket_list
16
+ # => "<foo>hi!</foo>"
17
+ #
18
+ # Alternatively you can treat the fixture module like a hash
19
+ #
20
+ # Fixtures::Buckets[:empty_bucket_list]
21
+ # => "<foo>hi!</foo>"
22
+ #
23
+ # You can find out all available fixtures by calling
24
+ #
25
+ # Fixtures.fixtures
26
+ # => ["Buckets"]
27
+ #
28
+ # And all the fixtures contained in a given fixture by calling
29
+ #
30
+ # Fixtures::Buckets.fixtures
31
+ # => ["bucket_list_with_more_than_one_bucket", "bucket_list_with_one_bucket", "empty_bucket_list"]
32
+ module Fixtures
33
+ class << self
34
+ def create_fixtures
35
+ files.each do |file|
36
+ create_fixture_for(file)
37
+ end
38
+ end
39
+
40
+ def create_fixture_for(file)
41
+ fixtures = YAML.load_file(path(file))
42
+ fixture_module = Module.new
43
+
44
+ fixtures.each do |name, value|
45
+ fixture_module.module_eval(<<-EVAL, __FILE__, __LINE__)
46
+ def #{name}
47
+ #{value.inspect}
48
+ end
49
+ module_function :#{name}
50
+ EVAL
51
+ end
52
+
53
+ fixture_module.module_eval(<<-EVAL, __FILE__, __LINE__)
54
+ module_function
55
+
56
+ def fixtures
57
+ #{fixtures.keys.sort.inspect}
58
+ end
59
+
60
+ def [](name)
61
+ send(name) if fixtures.include?(name.to_s)
62
+ end
63
+ EVAL
64
+
65
+ const_set(module_name(file), fixture_module)
66
+ end
67
+
68
+ def fixtures
69
+ constants.sort
70
+ end
71
+
72
+ private
73
+
74
+ def files
75
+ Dir.glob(File.dirname(__FILE__) + '/fixtures/*.yml').map {|fixture| File.basename(fixture)}
76
+ end
77
+
78
+ def module_name(file_name)
79
+ File.basename(file_name, '.*').capitalize
80
+ end
81
+
82
+ def path(file_name)
83
+ File.join(File.dirname(__FILE__), 'fixtures', file_name)
84
+ end
85
+ end
86
+
87
+ create_fixtures
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,133 @@
1
+ empty_bucket_list: >
2
+ <ListAllMyBucketsResult xmlns="http://oss.aliyuncs.com/doc/2006-03-01/">
3
+ <Owner>
4
+ <ID>ab00c3106e091f8fe23154c85678cda66628adb330bc00f02cf4a1c36d76bc48</ID>
5
+ <DisplayName>aliyun</DisplayName>
6
+ </Owner>
7
+ <Buckets/>
8
+ </ListAllMyBucketsResult>
9
+
10
+
11
+ bucket_list_with_one_bucket: >
12
+ <ListAllMyBucketsResult xmlns="http://oss.aliyuncs.com/doc/2006-03-01/">
13
+ <Owner>
14
+ <ID>ab00c3106e091f8fe23154c85678cda66628adb330bc00f02cf4a1c36d76bc48</ID>
15
+ <DisplayName>aliyun</DisplayName>
16
+ </Owner>
17
+ <Buckets>
18
+ <Bucket>
19
+ <Name>marcel_molina</Name>
20
+ <CreationDate>2006-10-04T15:58:38.000Z</CreationDate>
21
+ </Bucket>
22
+ </Buckets>
23
+ </ListAllMyBucketsResult>
24
+
25
+
26
+ bucket_list_with_more_than_one_bucket: >
27
+ <ListAllMyBucketsResult xmlns="http://oss.aliyuncs.com/doc/2006-03-01/">
28
+ <Owner>
29
+ <ID>ab00c3106e091f8fe23154c85678cda66628adb330bc00f02cf4a1c36d76bc48</ID>
30
+ <DisplayName>aliyun</DisplayName>
31
+ </Owner>
32
+ <Buckets>
33
+ <Bucket>
34
+ <Name>marcel_molina</Name>
35
+ <CreationDate>2006-10-04T15:58:38.000Z</CreationDate>
36
+ </Bucket>
37
+ <Bucket>
38
+ <Name>marcel_molina_jr</Name>
39
+ <CreationDate>2006-10-04T16:01:30.000Z</CreationDate>
40
+ </Bucket>
41
+ </Buckets>
42
+ </ListAllMyBucketsResult>
43
+
44
+ empty_bucket: >
45
+ <ListBucketResult xmlns="http://oss.aliyuncs.com/doc/2006-03-01/">
46
+ <Name>marcel_molina</Name>
47
+ <Prefix></Prefix>
48
+ <Marker></Marker>
49
+ <MaxKeys>1000</MaxKeys>
50
+ <IsTruncated>false</IsTruncated>
51
+ </ListBucketResult>
52
+
53
+ bucket_with_one_key: >
54
+ <ListBucketResult xmlns="http://oss.aliyuncs.com/doc/2006-03-01/">
55
+ <Name>marcel_molina</Name>
56
+ <Prefix></Prefix>
57
+ <Marker></Marker>
58
+ <MaxKeys>1000</MaxKeys>
59
+ <IsTruncated>false</IsTruncated>
60
+ <Contents>
61
+ <Key>tongue_overload.jpg</Key>
62
+ <LastModified>2006-10-05T02:42:22.000Z</LastModified>
63
+ <ETag>&quot;f21f7c4e8ea6e34b268887b07d6da745&quot;</ETag>
64
+ <Size>60673</Size>
65
+ <Owner>
66
+ <ID>bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1</ID>
67
+ <DisplayName>mmolina@onramp.net</DisplayName>
68
+ </Owner>
69
+ <StorageClass>STANDARD</StorageClass>
70
+ </Contents>
71
+ </ListBucketResult>
72
+
73
+ bucket_with_more_than_one_key: >
74
+ <ListBucketResult xmlns="http://oss.aliyuncs.com/doc/2006-03-01/">
75
+ <Name>marcel_molina</Name>
76
+ <Prefix></Prefix>
77
+ <Marker></Marker>
78
+ <MaxKeys>1000</MaxKeys>
79
+ <IsTruncated>false</IsTruncated>
80
+ <Contents>
81
+ <Key>beluga_baby.jpg</Key>
82
+ <LastModified>2006-10-05T02:55:10.000Z</LastModified>
83
+ <ETag>&quot;b2453d4a39a7387674a8c505112a2f0b&quot;</ETag>
84
+ <Size>35807</Size>
85
+ <Owner>
86
+ <ID>bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1</ID>
87
+ <DisplayName>mmolina@onramp.net</DisplayName>
88
+ </Owner>
89
+ <StorageClass>STANDARD</StorageClass>
90
+ </Contents>
91
+ <Contents>
92
+ <Key>tongue_overload.jpg</Key>
93
+ <LastModified>2006-10-05T02:42:22.000Z</LastModified>
94
+ <ETag>&quot;f21f7c4e8ea6e34b268887b07d6da745&quot;</ETag>
95
+ <Size>60673</Size>
96
+ <Owner>
97
+ <ID>bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1</ID>
98
+ <DisplayName>mmolina@onramp.net</DisplayName>
99
+ </Owner>
100
+ <StorageClass>STANDARD</StorageClass>
101
+ </Contents>
102
+ </ListBucketResult>
103
+
104
+ truncated_bucket_with_more_than_one_key: >
105
+ <ListBucketResult xmlns="http://oss.aliyuncs.com/doc/2006-03-01/">
106
+ <Name>marcel_molina</Name>
107
+ <Prefix></Prefix>
108
+ <Marker></Marker>
109
+ <MaxKeys>2</MaxKeys>
110
+ <IsTruncated>true</IsTruncated>
111
+ <Contents>
112
+ <Key>beluga_baby.jpg</Key>
113
+ <LastModified>2006-10-05T02:55:10.000Z</LastModified>
114
+ <ETag>&quot;b2453d4a39a7387674a8c505112a2f0b&quot;</ETag>
115
+ <Size>35807</Size>
116
+ <Owner>
117
+ <ID>bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1</ID>
118
+ <DisplayName>mmolina@onramp.net</DisplayName>
119
+ </Owner>
120
+ <StorageClass>STANDARD</StorageClass>
121
+ </Contents>
122
+ <Contents>
123
+ <Key>tongue_overload.jpg</Key>
124
+ <LastModified>2006-10-05T02:42:22.000Z</LastModified>
125
+ <ETag>&quot;f21f7c4e8ea6e34b268887b07d6da745&quot;</ETag>
126
+ <Size>60673</Size>
127
+ <Owner>
128
+ <ID>bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1</ID>
129
+ <DisplayName>mmolina@onramp.net</DisplayName>
130
+ </Owner>
131
+ <StorageClass>STANDARD</StorageClass>
132
+ </Contents>
133
+ </ListBucketResult>