Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / pithos / test.py @ 96befbfd

History | View | Annotate | Download (43.6 kB)

1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from unittest import TestCase
35
from mock import patch, call
36
from tempfile import NamedTemporaryFile
37
from os import urandom
38

    
39
from kamaki.clients import ClientError
40
from kamaki.clients.pithos import PithosClient as PC
41
from kamaki.clients.connection.kamakicon import KamakiHTTPConnection as C
42

    
43
client_pkg = 'kamaki.clients.Client'
44
pithos_pkg = 'kamaki.clients.pithos.PithosClient'
45

    
46
user_id = 'ac0un7-1d-5tr1ng'
47
obj = 'obj3c7N4m3'
48

    
49
account_info = {
50
    'content-language': 'en-us',
51
    'content-type': 'text/html; charset=utf-8',
52
    'date': 'Wed, 06 Mar 2013 13:25:51 GMT',
53
    'last-modified': 'Mon, 04 Mar 2013 18:22:31 GMT',
54
    'server': 'gunicorn/0.14.5',
55
    'vary': 'Accept-Language',
56
    'x-account-bytes-used': '751615526',
57
    'x-account-container-count': 7,
58
    'x-account-policy-quota': 53687091200,
59
    'x-account-policy-versioning': 'auto'}
60
container_info = {
61
    'content-language': 'en-us',
62
    'content-type': 'text/html; charset=utf-8',
63
    'date': 'Wed, 06 Mar 2013 15:11:05 GMT',
64
    'last-modified': 'Wed, 27 Feb 2013 15:56:13 GMT',
65
    'server': 'gunicorn/0.14.5',
66
    'vary': 'Accept-Language',
67
    'x-container-block-hash': 'sha256',
68
    'x-container-block-size': 4194304,
69
    'x-container-bytes-used': 309528938,
70
    'x-container-object-count': 14,
71
    'x-container-object-meta': '',
72
    'x-container-policy-quota': 53687091200,
73
    'x-container-policy-versioning': 'auto'}
74
object_info = {
75
    'content-language': 'en-us',
76
    'content-length': 254965,
77
    'content-type': 'application/octet-stream',
78
    'date': 'Thu, 07 Mar 2013 13:27:43 GMT',
79
    'etag': '',
80
    'last-modified': 'Mon, 04 Mar 2013 18:22:31 GMT',
81
    'server': 'gunicorn/0.14.5',
82
    'vary': 'Accept-Language',
83
    'x-object-hash': 'obj3c7h45h1s0bj3c7h45h411r34dY',
84
    'x-object-uuid': 'd0c747ca-34bd-49e0-8e98-1d07d8b0cbc7',
85
    'x-object-version': '525996',
86
    'x-object-version-timestamp': 'Mon, 04 Mar 2013 18:22:31 GMT',
87
    'x-object-meta-k1': 'v1',
88
    'x-object-meta-k2': 'v2'}
89
container_list = [
90
    dict(
91
        count=2,
92
        last_modified="2013-02-27T11:56:09.893033+00:00",
93
        bytes=677076979,
94
        name="pithos",
95
        x_container_policy=dict(quota="21474836480", versioning="auto")),
96
    dict(
97
        count=0,
98
        last_modified="2012-10-23T12:25:17.229187+00:00",
99
        bytes=0,
100
        name="trash",
101
        x_container_policy=dict(quota="21474836480", versioning="auto"))]
102
object_list = [
103
    dict(hash="",
104
        name="The_Secret_Garden.zip",
105
        x_object_public="/public/wdp9p",
106
        bytes=203304947,
107
        x_object_version_timestamp="1360237915.7027509",
108
        x_object_uuid="s0m3uu1df0r0bj0n3",
109
        last_modified="2013-02-07T11:51:55.702751+00:00",
110
        content_type="application/octet-stream",
111
        x_object_hash="0afdf29f71cd53126225c3f54ca",
112
        x_object_version=17737,
113
        x_object_modified_by=user_id),
114
    dict(hash="",
115
        name="The_Revealed_Garden.zip",
116
        x_object_public="/public/wpd7p",
117
        bytes=20330947,
118
        x_object_version_timestamp="13602915.7027509",
119
        x_object_uuid="s0m3uu1df0r0bj70w",
120
        last_modified="2013-02-07T11:51:55.702751+00:00",
121
        content_type="application/octet-stream",
122
        x_object_hash="0afdf29f71cd53126225c3f54ca",
123
        x_object_version=17737,
124
        x_object_modified_by=user_id)]
125
object_hashmap = dict(
126
    block_hash="sha256", block_size=4194304, bytes=33554432,
127
    hashes=[
128
        "4988438cc1c0292c085d289649b28cf547ba3db71c6efaac9f2df7e193d4d0af",
129
        "b214244aa56df7d1df7c6cac066e7cef268d9c2beb4dcf7ce68af667b0626f91",
130
        "17f365f25e0682565ded30576066bb13377a3d306967e4d74e06bb6bbc20f75f",
131
        "2524ae208932669fff89adf8a2fc0df3b67736ca0d3aadce7a2ce640f142af37",
132
        "5d807a2129d2fcd3c221c3da418ed52af3fc48d0817b62e0bb437acffccd3514",
133
        "609de22ce842d997f645fc49d5f14e0e3766dd51a6cbe66383b2bab82c8dfcd0",
134
        "3102851ac168c78be70e35ff5178c7b1ebebd589e5106d565ca1094d1ca8ff59",
135
        "bfe306dd24e92a8d85caf7055643f250fd319e8c4cdd4755ddabbf3ff97e83c7"])
136
sharers = [
137
    dict(last_modified="2013-01-29T16:50:06.084674+00:00", name="0b1a-82d5"),
138
    dict(last_modified="2013-01-29T16:50:06.084674+00:00", name="0b2a-f2d5"),
139
    dict(last_modified="2013-01-29T16:50:06.084674+00:00", name="2b1a-82d6")]
140

    
141

    
142
class FR(object):
143
    """FR stands for Fake Response"""
144
    json = dict()
145
    headers = dict()
146
    content = json
147
    status = None
148
    status_code = 200
149

    
150
    def release(self):
151
        pass
152

    
153

    
154
class Pithos(TestCase):
155

    
156
    files = []
157

    
158
    def _create_temp_file(self, num_of_blocks):
159
        self.files.append(NamedTemporaryFile())
160
        tmpFile = self.files[-1]
161
        file_size = num_of_blocks * 4 * 1024 * 1024
162
        print('\n\tCreate tmp file')
163
        tmpFile.write(urandom(file_size))
164
        tmpFile.flush()
165
        tmpFile.seek(0)
166
        print('\t\tDone')
167
        return tmpFile
168

    
169
    def assert_dicts_are_equal(self, d1, d2):
170
        for k, v in d1.items():
171
            self.assertTrue(k in d2)
172
            if isinstance(v, dict):
173
                self.assert_dicts_are_equal(v, d2[k])
174
            else:
175
                self.assertEqual(unicode(v), unicode(d2[k]))
176

    
177
    def setUp(self):
178
        self.url = 'https://www.example.com/pithos'
179
        self.token = 'p17h0570k3n'
180
        self.client = PC(self.url, self.token)
181
        self.client.account = user_id
182
        self.client.container = 'c0nt@1n3r_i'
183

    
184
    def tearDown(self):
185
        FR.headers = dict()
186
        FR.status_code = 200
187
        FR.json = dict()
188
        FR.content = FR.json
189
        for f in self.files:
190
            f.close()
191

    
192
    #  Pithos+ methods that extend storage API
193

    
194
    @patch('%s.account_head' % pithos_pkg, return_value=FR())
195
    def test_get_account_info(self, AH):
196
        FR.headers = account_info
197
        r = self.client.get_account_info()
198
        self.assert_dicts_are_equal(r, account_info)
199
        self.assertEqual(AH.mock_calls[-1], call(until=None))
200
        unt = 'un71L-d473'
201
        r = self.client.get_account_info(until=unt)
202
        self.assert_dicts_are_equal(r, account_info)
203
        self.assertEqual(AH.mock_calls[-1], call(until=unt))
204
        FR.status_code = 401
205
        self.assertRaises(ClientError, self.client.get_account_info)
206

    
207
    @patch('%s.account_post' % pithos_pkg, return_value=FR())
208
    def test_del_account_meta(self, ap):
209
        keys = ['k1', 'k2', 'k3']
210
        expected = []
211
        for key in keys:
212
            self.client.del_account_meta(key)
213
            expected.append(call(update=True, metadata={key: ''}))
214
        self.assertEqual(ap.mock_calls, expected)
215

    
216
    @patch('%s.container_head' % pithos_pkg, return_value=FR())
217
    def test_get_container_info(self, ch):
218
        FR.headers = container_info
219
        r = self.client.get_container_info()
220
        self.assert_dicts_are_equal(r, container_info)
221
        u = 'some date'
222
        r = self.client.get_container_info(until=u)
223
        self.assertEqual(ch.mock_calls, [call(until=None), call(until=u)])
224

    
225
    @patch('%s.delete' % pithos_pkg, return_value=FR())
226
    def test_delete_container(self, delete):
227
        FR.status_code = 204
228
        cont = 's0m3c0n731n3r'
229
        self.client.delete_container(cont)
230
        for err_code in (404, 409):
231
            FR.status_code = err_code
232
            self.assertRaises(ClientError, self.client.delete_container, cont)
233
        acall = call('/%s/%s' % (user_id, cont), success=(204, 404, 409))
234
        self.assertEqual(delete.mock_calls, [acall] * 3)
235

    
236
    @patch('%s.account_get' % pithos_pkg, return_value=FR())
237
    def test_list_containers(self, get):
238
        FR.json = container_list
239
        r = self.client.list_containers()
240
        for i in range(len(r)):
241
            self.assert_dicts_are_equal(r[i], container_list[i])
242

    
243
    @patch('%s.get_container_info' % pithos_pkg, return_value=container_info)
244
    @patch('%s.container_post' % pithos_pkg, return_value=FR())
245
    @patch('%s.object_put' % pithos_pkg, return_value=FR())
246
    def test_upload_object(self, CI, CP, OP):
247
        num_of_blocks = 8
248
        tmpFile = self._create_temp_file(num_of_blocks)
249

    
250
        # Without kwargs
251
        self.client.upload_object(obj, tmpFile)
252
        self.assertEqual(PC.get_container_info.mock_calls, [call()])
253
        [call1, call2] = PC.object_put.mock_calls
254

    
255
        (args1, kwargs1) = call1[1:3]
256
        (args2, kwargs2) = call2[1:3]
257
        self.assertEqual(args1, (obj,))
258
        expected1 = dict(
259
            hashmap=True,
260
            success=(201, 409),
261
            format='json',
262
            json=dict(
263
                hashes=['s0m3h@5h'] * num_of_blocks,
264
                bytes=num_of_blocks * 4 * 1024 * 1024),
265
            etag=None,
266
            content_encoding=None,
267
            content_type='application/octet-stream',
268
            content_disposition=None,
269
            public=None,
270
            permissions=None)
271
        for k, v in expected1.items():
272
            if k == 'json':
273
                self.assertEqual(len(v['hashes']), len(kwargs1[k]['hashes']))
274
                self.assertEqual(v['bytes'], kwargs1[k]['bytes'])
275
            else:
276
                self.assertEqual(v, kwargs1[k])
277

    
278
        (args2, kwargs2) = call2[1:3]
279
        self.assertEqual(args2, (obj,))
280
        expected2 = dict(
281
            json=dict(
282
                hashes=['s0m3h@5h'] * num_of_blocks,
283
                bytes=num_of_blocks * 4 * 1024 * 1024),
284
            content_type='application/octet-stream',
285
            hashmap=True,
286
            success=201,
287
            format='json')
288
        for k, v in expected2.items():
289
            if k == 'json':
290
                self.assertEqual(len(v['hashes']), len(kwargs2[k]['hashes']))
291
                self.assertEqual(v['bytes'], kwargs2[k]['bytes'])
292
            else:
293
                self.assertEqual(v, kwargs2[k])
294

    
295
        OP = PC.object_put
296
        mock_offset = 2
297

    
298
        #  With progress bars
299
        try:
300
            from progress.bar import ShadyBar
301
            blck_bar = ShadyBar('Mock blck calc.')
302
            upld_bar = ShadyBar('Mock uplds')
303
        except ImportError:
304
            blck_bar = None
305
            upld_bar = None
306

    
307
        if blck_bar and upld_bar:
308

    
309
            def blck_gen(n):
310
                for i in blck_bar.iter(range(n)):
311
                    yield
312
                yield
313

    
314
            def upld_gen(n):
315
                for i in upld_bar.iter(range(n)):
316
                    yield
317
                yield
318

    
319
            tmpFile.seek(0)
320
            self.client.upload_object(
321
                obj, tmpFile,
322
                hash_cb=blck_gen, upload_cb=upld_gen)
323

    
324
            for i, c in enumerate(OP.mock_calls[-mock_offset:]):
325
                self.assertEqual(OP.mock_calls[i], c)
326

    
327
        #  With content-type
328
        tmpFile.seek(0)
329
        ctype = 'video/mpeg'
330
        sharing = dict(read=['u1', 'g1', 'u2'], write=['u1'])
331
        self.client.upload_object(obj, tmpFile,
332
            content_type=ctype, sharing=sharing)
333
        self.assertEqual(OP.mock_calls[-1][2]['content_type'], ctype)
334
        self.assert_dicts_are_equal(
335
            OP.mock_calls[-2][2]['permissions'],
336
            sharing)
337

    
338
        # With other args
339
        tmpFile.seek(0)
340
        kwargs = dict(
341
            etag='s0m3E74g',
342
            content_type=ctype,
343
            content_disposition=ctype + 'd15p051710n',
344
            public=True,
345
            content_encoding='802.11')
346
        self.client.upload_object(obj, tmpFile, **kwargs)
347
        for arg, val in kwargs.items():
348
            self.assertEqual(OP.mock_calls[-2][2][arg], val)
349

    
350
    @patch('%s.put' % pithos_pkg, return_value=FR())
351
    @patch('%s.set_header' % client_pkg)
352
    def test_create_object(self, SH, put):
353
        cont = self.client.container
354
        ctype = 'c0n73n7/typ3'
355
        exp_shd = [
356
            call('Content-Type', 'application/octet-stream'),
357
            call('Content-length', '0'),
358
            call('Content-Type', ctype), call('Content-length', '42')]
359
        exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)] * 2
360
        self.client.create_object(obj)
361
        self.client.create_object(obj, content_type=ctype, content_length=42)
362
        self.assertEqual(PC.set_header.mock_calls, exp_shd)
363
        self.assertEqual(put.mock_calls, exp_put)
364

    
365
    @patch('%s.put' % pithos_pkg, return_value=FR())
366
    @patch('%s.set_header' % client_pkg)
367
    def test_create_directory(self, SH, put):
368
        cont = self.client.container
369
        exp_shd = [
370
            call('Content-Type', 'application/directory'),
371
            call('Content-length', '0')]
372
        exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)]
373
        self.client.create_directory(obj)
374
        self.assertEqual(PC.set_header.mock_calls, exp_shd)
375
        self.assertEqual(put.mock_calls, exp_put)
376

    
377
    def test_get_object_info(self):
378
        FR.headers = object_info
379
        version = 'v3r510n'
380
        with patch.object(PC, 'object_head', return_value=FR()) as head:
381
            r = self.client.get_object_info(obj)
382
            self.assertEqual(r, object_info)
383
            r = self.client.get_object_info(obj, version=version)
384
            self.assertEqual(head.mock_calls, [
385
                call(obj, version=None),
386
                call(obj, version=version)])
387
        with patch.object(
388
                PC,
389
                'object_head',
390
                side_effect=ClientError('Obj not found', 404)):
391
            self.assertRaises(
392
                ClientError,
393
                self.client.get_object_info,
394
                obj, version=version)
395

    
396
    @patch('%s.get_object_info' % pithos_pkg, return_value=object_info)
397
    def test_get_object_meta(self, GOI):
398
        expected = dict()
399
        for k, v in object_info.items():
400
            expected[k] = v
401
        r = self.client.get_object_meta(obj)
402
        self.assert_dicts_are_equal(r, expected)
403

    
404
    @patch('%s.object_post' % pithos_pkg, return_value=FR())
405
    def test_del_object_meta(self, post):
406
        metakey = '50m3m3t4k3y'
407
        self.client.del_object_meta(obj, metakey)
408
        expected = call(obj, update=True, metadata={metakey: ''})
409
        self.assertEqual(post.mock_calls[-1], expected)
410

    
411
    @patch('%s.post' % client_pkg, return_value=FR())
412
    @patch('%s.set_header' % client_pkg)
413
    def test_replace_object_meta(self, SH, post):
414
        metas = dict(k1='new1', k2='new2', k3='new3')
415
        cont = self.client.container
416
        self.client.replace_object_meta(metas)
417
        expected = call('/%s/%s' % (user_id, cont), success=202)
418
        self.assertEqual(post.mock_calls[-1], expected)
419
        prfx = 'X-Object-Meta-'
420
        expected = [call('%s%s' % (prfx, k), v) for k, v in metas.items()]
421
        self.assertEqual(PC.set_header.mock_calls, expected)
422

    
423
    @patch('%s.object_put' % pithos_pkg, return_value=FR())
424
    def test_copy_object(self, put):
425
        src_cont = 'src-c0nt41n3r'
426
        src_obj = 'src-0bj'
427
        dst_cont = 'dst-c0nt41n3r'
428
        dst_obj = 'dst-0bj'
429
        expected = call(
430
            src_obj,
431
            content_length=0,
432
            source_account=None,
433
            success=201,
434
            copy_from='/%s/%s' % (src_cont, src_obj),
435
            delimiter=None,
436
            content_type=None,
437
            source_version=None,
438
            public=False)
439
        self.client.copy_object(src_cont, src_obj, dst_cont)
440
        self.assertEqual(put.mock_calls[-1], expected)
441
        self.client.copy_object(src_cont, src_obj, dst_cont, dst_obj)
442
        self.assertEqual(put.mock_calls[-1][1], (dst_obj,))
443
        kwargs = dict(
444
            source_version='src-v3r510n',
445
            source_account='src-4cc0un7',
446
            public=True,
447
            content_type='c0n73n7Typ3',
448
            delimiter='5')
449
        self.client.copy_object(src_cont, src_obj, dst_cont, **kwargs)
450
        for k, v in kwargs.items():
451
            self.assertEqual(v, put.mock_calls[-1][2][k])
452

    
453
    @patch('%s.object_put' % pithos_pkg, return_value=FR())
454
    def test_move_object(self, put):
455
        src_cont = 'src-c0nt41n3r'
456
        src_obj = 'src-0bj'
457
        dst_cont = 'dst-c0nt41n3r'
458
        dst_obj = 'dst-0bj'
459
        expected = call(
460
            src_obj,
461
            content_length=0,
462
            source_account=None,
463
            success=201,
464
            move_from='/%s/%s' % (src_cont, src_obj),
465
            delimiter=None,
466
            content_type=None,
467
            source_version=None,
468
            public=False)
469
        self.client.move_object(src_cont, src_obj, dst_cont)
470
        self.assertEqual(put.mock_calls[-1], expected)
471
        self.client.move_object(src_cont, src_obj, dst_cont, dst_obj)
472
        self.assertEqual(put.mock_calls[-1][1], (dst_obj,))
473
        kwargs = dict(
474
            source_version='src-v3r510n',
475
            source_account='src-4cc0un7',
476
            public=True,
477
            content_type='c0n73n7Typ3',
478
            delimiter='5')
479
        self.client.move_object(src_cont, src_obj, dst_cont, **kwargs)
480
        for k, v in kwargs.items():
481
            self.assertEqual(v, put.mock_calls[-1][2][k])
482

    
483
    @patch('%s.delete' % client_pkg, return_value=FR())
484
    def test_delete_object(self, delete):
485
        cont = self.client.container
486
        self.client.delete_object(obj)
487
        self.assertEqual(
488
            delete.mock_calls[-1],
489
            call('/%s/%s/%s' % (user_id, cont, obj), success=(204, 404)))
490
        FR.status_code = 404
491
        self.assertRaises(ClientError, self.client.delete_object, obj)
492

    
493
    @patch('%s.get' % client_pkg, return_value=FR())
494
    @patch('%s.set_param' % client_pkg)
495
    def test_list_objects(self, SP, get):
496
        FR.json = object_list
497
        acc = self.client.account
498
        cont = self.client.container
499
        SP = PC.set_param
500
        r = self.client.list_objects()
501
        for i in range(len(r)):
502
            self.assert_dicts_are_equal(r[i], object_list[i])
503
        self.assertEqual(get.mock_calls, [
504
            call('/%s/%s' % (acc, cont), success=(200, 204, 304, 404))])
505
        self.assertEqual(SP.mock_calls, [call('format', 'json')])
506
        FR.status_code = 304
507
        self.assertEqual(self.client.list_objects(), [])
508
        FR.status_code = 404
509
        self.assertRaises(ClientError, self.client.list_objects)
510

    
511
    @patch('%s.get' % client_pkg, return_value=FR())
512
    @patch('%s.set_param' % client_pkg)
513
    def test_list_objects_in_path(self, SP, get):
514
        FR.json = object_list
515
        path = '/some/awsome/path'
516
        acc = self.client.account
517
        cont = self.client.container
518
        SP = PC.set_param
519
        self.client.list_objects_in_path(path)
520
        self.assertEqual(get.mock_calls, [
521
            call('/%s/%s' % (acc, cont), success=(200, 204, 404))])
522
        self.assertEqual(SP.mock_calls, [
523
            call('format', 'json'), call('path', path)])
524
        FR.status_code = 404
525
        self.assertRaises(ClientError, self.client.list_objects)
526

    
527
    #  Pithos+ only methods
528

    
529
    @patch('%s.container_delete' % pithos_pkg, return_value=FR())
530
    def test_purge_container(self, cd):
531
        self.client.purge_container()
532
        self.assertTrue('until' in cd.mock_calls[-1][2])
533
        cont = self.client.container
534
        self.client.purge_container('another-container')
535
        self.assertEqual(self.client.container, cont)
536

    
537
    @patch('%s.object_put' % pithos_pkg, return_value=FR())
538
    def test_upload_object_unchunked(self, put):
539
        num_of_blocks = 8
540
        tmpFile = self._create_temp_file(num_of_blocks)
541
        expected = dict(
542
                success=201,
543
                data=num_of_blocks * 4 * 1024 * 1024,
544
                etag='some-etag',
545
                content_encoding='some content_encoding',
546
                content_type='some content-type',
547
                content_disposition='some content_disposition',
548
                public=True,
549
                permissions=dict(read=['u1', 'g1', 'u2'], write=['u1']))
550
        self.client.upload_object_unchunked(obj, tmpFile)
551
        self.assertEqual(put.mock_calls[-1][1], (obj,))
552
        self.assertEqual(
553
            sorted(put.mock_calls[-1][2].keys()),
554
            sorted(expected.keys()))
555
        kwargs = dict(expected)
556
        kwargs.pop('success')
557
        kwargs['size'] = kwargs.pop('data')
558
        kwargs['sharing'] = kwargs.pop('permissions')
559
        tmpFile.seek(0)
560
        self.client.upload_object_unchunked(obj, tmpFile, **kwargs)
561
        pmc = put.mock_calls[-1][2]
562
        for k, v in expected.items():
563
            if k == 'data':
564
                self.assertEqual(len(pmc[k]), v)
565
            else:
566
                self.assertEqual(pmc[k], v)
567
        self.assertRaises(
568
            ClientError,
569
            self.client.upload_object_unchunked,
570
            obj, tmpFile, withHashFile=True)
571

    
572
    @patch('%s.object_put' % pithos_pkg, return_value=FR())
573
    def test_create_object_by_manifestation(self, put):
574
        manifest = '%s/%s' % (self.client.container, obj)
575
        kwargs = dict(
576
                etag='some-etag',
577
                content_encoding='some content_encoding',
578
                content_type='some content-type',
579
                content_disposition='some content_disposition',
580
                public=True,
581
                sharing=dict(read=['u1', 'g1', 'u2'], write=['u1']))
582
        self.client.create_object_by_manifestation(obj)
583
        expected = dict(content_length=0, manifest=manifest)
584
        for k in kwargs:
585
            expected['permissions' if k == 'sharing' else k] = None
586
        self.assertEqual(put.mock_calls[-1], call(obj, **expected))
587
        self.client.create_object_by_manifestation(obj, **kwargs)
588
        expected.update(kwargs)
589
        expected['permissions'] = expected.pop('sharing')
590
        self.assertEqual(put.mock_calls[-1], call(obj, **expected))
591

    
592
    @patch('%s.get_object_hashmap' % pithos_pkg, return_value=object_hashmap)
593
    @patch('%s.object_get' % pithos_pkg, return_value=FR())
594
    def test_download_object(self, GOH, GET):
595
        num_of_blocks = 8
596
        tmpFile = self._create_temp_file(num_of_blocks)
597
        FR.content = tmpFile.read(4 * 1024 * 1024)
598
        tmpFile = self._create_temp_file(num_of_blocks)
599
        GET = PC.object_get
600
        num_of_blocks = len(object_hashmap['hashes'])
601
        kwargs = dict(
602
            resume=True,
603
            version='version',
604
            range_str='10-20',
605
            if_match='if and only if',
606
            if_none_match='if and only not',
607
            if_modified_since='what if not?',
608
            if_unmodified_since='this happens if not!',
609
            async_headers=dict(Range='bytes=0-88888888'))
610

    
611
        self.client.download_object(obj, tmpFile)
612
        self.assertEqual(len(GET.mock_calls), num_of_blocks)
613
        self.assertEqual(GET.mock_calls[-1][1], (obj,))
614
        for k, v in kwargs.items():
615
            if k == 'async_headers':
616
                self.assertTrue('Range' in GET.mock_calls[-1][2][k])
617
            elif k in ('resume', 'range_str'):
618
                continue
619
            else:
620
                self.assertEqual(GET.mock_calls[-1][2][k], None)
621

    
622
        #  Check ranges are consecutive
623
        starts = []
624
        ends = []
625
        for c in GET.mock_calls:
626
            rng_str = c[2]['async_headers']['Range']
627
            (start, rng_str) = rng_str.split('=')
628
            (start, end) = rng_str.split('-')
629
            starts.append(start)
630
            ends.append(end)
631
        ends = sorted(ends)
632
        for i, start in enumerate(sorted(starts)):
633
            if i:
634
                int(ends[i - 1]) == int(start) - 1
635

    
636
        #  With progress bars
637
        try:
638
            from progress.bar import ShadyBar
639
            dl_bar = ShadyBar('Mock dl')
640
        except ImportError:
641
            dl_bar = None
642

    
643
        if dl_bar:
644

    
645
            def blck_gen(n):
646
                for i in dl_bar.iter(range(n)):
647
                    yield
648
                yield
649

    
650
            tmpFile.seek(0)
651
            self.client.download_object(obj, tmpFile, download_cb=blck_gen)
652
            self.assertEqual(len(GET.mock_calls), 2 * num_of_blocks)
653

    
654
        tmpFile.seek(0)
655
        kwargs.pop('async_headers')
656
        kwargs.pop('resume')
657
        self.client.download_object(obj, tmpFile, **kwargs)
658
        for k, v in kwargs.items():
659
            if k == 'range_str':
660
                self.assertEqual(
661
                    GET.mock_calls[-1][2]['data_range'],
662
                    'bytes=%s' % v)
663
            else:
664
                self.assertEqual(GET.mock_calls[-1][2][k], v)
665

    
666
        #  ALl options on no tty
667

    
668
        def foo():
669
            return True
670

    
671
        tmpFile.seek(0)
672
        tmpFile.isatty = foo
673
        self.client.download_object(obj, tmpFile, **kwargs)
674
        for k, v in kwargs.items():
675
            if k == 'range_str':
676
                self.assertTrue('data_range' in GET.mock_calls[-1][2])
677
            else:
678
                self.assertEqual(GET.mock_calls[-1][2][k], v)
679

    
680
    def test_get_object_hashmap(self):
681
        FR.json = object_hashmap
682
        for empty in (304, 412):
683
            with patch.object(
684
                    PC, 'object_get',
685
                    side_effect=ClientError('Empty', status=empty)):
686
                r = self.client.get_object_hashmap(obj)
687
                self.assertEqual(r, {})
688
        exp_args = dict(
689
            hashmap=True,
690
            data_range=None,
691
            version=None,
692
            if_etag_match=None,
693
            if_etag_not_match=None,
694
            if_modified_since=None,
695
            if_unmodified_since=None)
696
        kwargs = dict(
697
            version='s0m3v3r51on',
698
            if_match='if match',
699
            if_none_match='if non match',
700
            if_modified_since='some date here',
701
            if_unmodified_since='some date here',
702
            data_range='10-20')
703
        with patch.object(PC, 'object_get', return_value=FR()) as get:
704
            r = self.client.get_object_hashmap(obj)
705
            self.assertEqual(r, object_hashmap)
706
            self.assertEqual(get.mock_calls[-1], call(obj, **exp_args))
707
            r = self.client.get_object_hashmap(obj, **kwargs)
708
            exp_args['if_etag_match'] = kwargs.pop('if_match')
709
            exp_args['if_etag_not_match'] = kwargs.pop('if_none_match')
710
            exp_args.update(kwargs)
711
            self.assertEqual(get.mock_calls[-1], call(obj, **exp_args))
712

    
713
    @patch('%s.account_post' % pithos_pkg, return_value=FR())
714
    def test_set_account_group(self, post):
715
        (group, usernames) = ('aU53rGr0up', ['u1', 'u2', 'u3'])
716
        self.client.set_account_group(group, usernames)
717
        self.assertEqual(
718
            post.mock_calls[-1],
719
            call(update=True, groups={group: usernames}))
720

    
721
    @patch('%s.account_post' % pithos_pkg, return_value=FR())
722
    def test_del_account_group(self, post):
723
        group = 'aU53rGr0up'
724
        self.client.del_account_group(group)
725
        self.assertEqual(
726
            post.mock_calls[-1],
727
            call(update=True, groups={group: []}))
728

    
729
    @patch('%s.get_account_info' % pithos_pkg, return_value=account_info)
730
    def test_get_account_quota(self, GAI):
731
        key = 'x-account-policy-quota'
732
        r = self.client.get_account_quota()
733
        self.assertEqual(r[key], account_info[key])
734

    
735
    @patch('%s.get_account_info' % pithos_pkg, return_value=account_info)
736
    def test_get_account_versioning(self, GAI):
737
        key = 'x-account-policy-versioning'
738
        r = self.client.get_account_versioning()
739
        self.assertEqual(r[key], account_info[key])
740

    
741
    def test_get_account_meta(self):
742
        key = 'x-account-meta-'
743
        with patch.object(PC, 'get_account_info', return_value=account_info):
744
            r = self.client.get_account_meta()
745
            keys = [k for k in r if k.startswith(key)]
746
            self.assertFalse(keys)
747
        acc_info = dict(account_info)
748
        acc_info['%sk1' % key] = 'v1'
749
        acc_info['%sk2' % key] = 'v2'
750
        acc_info['%sk3' % key] = 'v3'
751
        with patch.object(PC, 'get_account_info', return_value=acc_info):
752
            r = self.client.get_account_meta()
753
            for k in [k for k in acc_info if k.startswith(key)]:
754
                self.assertEqual(r[k], acc_info[k])
755

    
756
    def test_get_account_group(self):
757
        key = 'x-account-group-'
758
        with patch.object(PC, 'get_account_info', return_value=account_info):
759
            r = self.client.get_account_group()
760
            keys = [k for k in r if k.startswith(key)]
761
            self.assertFalse(keys)
762
        acc_info = dict(account_info)
763
        acc_info['%sk1' % key] = 'g1'
764
        acc_info['%sk2' % key] = 'g2'
765
        acc_info['%sk3' % key] = 'g3'
766
        with patch.object(PC, 'get_account_info', return_value=acc_info):
767
            r = self.client.get_account_group()
768
            for k in [k for k in acc_info if k.startswith(key)]:
769
                self.assertEqual(r[k], acc_info[k])
770

    
771
    @patch('%s.account_post' % pithos_pkg, return_value=FR())
772
    def test_set_account_meta(self, post):
773
        metas = dict(k1='v1', k2='v2', k3='v3')
774
        self.client.set_account_meta(metas)
775
        self.assertEqual(
776
            post.mock_calls[-1],
777
            call(update=True, metadata=metas))
778

    
779
    @patch('%s.account_post' % pithos_pkg, return_value=FR())
780
    def test_set_account_quota(self, post):
781
        qu = 1024
782
        self.client.set_account_quota(qu)
783
        self.assertEqual(post.mock_calls[-1], call(update=True, quota=qu))
784

    
785
    @patch('%s.account_post' % pithos_pkg, return_value=FR())
786
    def test_set_account_versioning(self, post):
787
        vrs = 'n3wV3r51on1ngTyp3'
788
        self.client.set_account_versioning(vrs)
789
        self.assertEqual(
790
            post.mock_calls[-1],
791
            call(update=True, versioning=vrs))
792

    
793
    @patch('%s.container_delete' % pithos_pkg, return_value=FR())
794
    def test_del_container(self, delete):
795
        for kwarg in (
796
                dict(delimiter=None, until=None),
797
                dict(delimiter='X', until='50m3d473')):
798
            self.client.del_container(**kwarg)
799
            expected = dict(kwarg)
800
            expected['success'] = (204, 404, 409)
801
            self.assertEqual(delete.mock_calls[-1], call(**expected))
802
        for status_code in (404, 409):
803
            FR.status_code = status_code
804
            self.assertRaises(ClientError, self.client.del_container)
805

    
806
    @patch('%s.get_container_info' % pithos_pkg, return_value=container_info)
807
    def test_get_container_versioning(self, GCI):
808
        key = 'x-container-policy-versioning'
809
        cont = 'c0n7-417'
810
        bu_cnt = self.client.container
811
        for container in (None, cont):
812
            r = self.client.get_container_versioning(container=container)
813
            self.assertEqual(r[key], container_info[key])
814
            self.assertEqual(GCI.mock_calls[-1], call())
815
            self.assertEqual(bu_cnt, self.client.container)
816

    
817
    @patch('%s.get_container_info' % pithos_pkg, return_value=container_info)
818
    def test_get_container_quota(self, GCI):
819
        key = 'x-container-policy-quota'
820
        cont = 'c0n7-417'
821
        bu_cnt = self.client.container
822
        for container in (None, cont):
823
            r = self.client.get_container_quota(container=container)
824
            self.assertEqual(r[key], container_info[key])
825
            self.assertEqual(GCI.mock_calls[-1], call())
826
            self.assertEqual(bu_cnt, self.client.container)
827

    
828
    def test_get_container_meta(self):
829
        somedate = '50m3d473'
830
        key = 'x-container-meta'
831
        metaval = '50m3m374v41'
832
        container_plus = dict(container_info)
833
        container_plus[key] = metaval
834
        for ret in ((container_info, {}), (container_plus, {key: metaval})):
835
            with patch.object(
836
                    PC,
837
                    'get_container_info',
838
                    return_value=ret[0]) as gci:
839
                for until in (None, somedate):
840
                    r = self.client.get_container_meta(until=until)
841
                    self.assertEqual(r, ret[1])
842
                    self.assertEqual(gci.mock_calls[-1], call(until=until))
843

    
844
    def test_get_container_object_meta(self):
845
        somedate = '50m3d473'
846
        key = 'x-container-object-meta'
847
        metaval = '50m3m374v41'
848
        container_plus = dict(container_info)
849
        container_plus[key] = metaval
850
        for ret in (
851
                (container_info, {key: ''}),
852
                (container_plus, {key: metaval})):
853
            with patch.object(
854
                    PC,
855
                    'get_container_info',
856
                    return_value=ret[0]) as gci:
857
                for until in (None, somedate):
858
                    r = self.client.get_container_object_meta(until=until)
859
                    self.assertEqual(r, ret[1])
860
                    self.assertEqual(gci.mock_calls[-1], call(until=until))
861

    
862
    @patch('%s.container_post' % pithos_pkg, return_value=FR())
863
    def test_set_container_meta(self, post):
864
        metas = dict(k1='v1', k2='v2', k3='v3')
865
        self.client.set_container_meta(metas)
866
        self.assertEqual(
867
            post.mock_calls[-1],
868
            call(update=True, metadata=metas))
869

    
870
    @patch('%s.container_post' % pithos_pkg, return_value=FR())
871
    def test_del_container_meta(self, ap):
872
        self.client.del_container_meta('somekey')
873
        expected = [call(update=True, metadata={'somekey': ''})]
874
        self.assertEqual(ap.mock_calls, expected)
875

    
876
    @patch('%s.container_post' % pithos_pkg, return_value=FR())
877
    def test_set_container_quota(self, post):
878
        qu = 1024
879
        self.client.set_container_quota(qu)
880
        self.assertEqual(post.mock_calls[-1], call(update=True, quota=qu))
881

    
882
    @patch('%s.container_post' % pithos_pkg, return_value=FR())
883
    def test_set_container_versioning(self, post):
884
        vrs = 'n3wV3r51on1ngTyp3'
885
        self.client.set_container_versioning(vrs)
886
        self.assertEqual(
887
            post.mock_calls[-1],
888
            call(update=True, versioning=vrs))
889

    
890
    @patch('%s.object_delete' % pithos_pkg, return_value=FR())
891
    def test_del_object(self, delete):
892
        for kwarg in (
893
                dict(delimiter=None, until=None),
894
                dict(delimiter='X', until='50m3d473')):
895
            self.client.del_object(obj, **kwarg)
896
            self.assertEqual(delete.mock_calls[-1], call(obj, **kwarg))
897

    
898
    @patch('%s.object_post' % pithos_pkg, return_value=FR())
899
    def test_set_object_meta(self, post):
900
        metas = dict(k1='v1', k2='v2', k3='v3')
901
        self.assertRaises(
902
            AssertionError,
903
            self.client.set_object_meta,
904
            obj, 'Non dict arg')
905
        self.client.set_object_meta(obj, metas)
906
        self.assertEqual(
907
            post.mock_calls[-1],
908
            call(obj, update=True, metadata=metas))
909

    
910
    @patch('%s.object_post' % pithos_pkg, return_value=FR())
911
    def test_publish_object(self, post):
912
        oinfo = dict(object_info)
913
        val = 'pubL1c'
914
        oinfo['x-object-public'] = val
915
        with patch.object(PC, 'get_object_info', return_value=oinfo) as gof:
916
            r = self.client.publish_object(obj)
917
            self.assertEqual(
918
                post.mock_calls[-1],
919
                call(obj, public=True, update=True))
920
            self.assertEqual(gof.mock_calls[-1], call(obj))
921
            self.assertEqual(r, '%s%s' % (self.url[:-6], val))
922

    
923
    @patch('%s.object_post' % pithos_pkg, return_value=FR())
924
    def test_unpublish_object(self, post):
925
        self.client.unpublish_object(obj)
926
        self.assertEqual(
927
            post.mock_calls[-1],
928
            call(obj, public=False, update=True))
929

    
930
    def test_get_object_sharing(self):
931
        info = dict(object_info)
932
        expected = dict(read='u1,g1,u2', write='u1')
933
        info['x-object-sharing'] = '; '.join(
934
            ['%s=%s' % (k, v) for k, v in expected.items()])
935
        with patch.object(PC, 'get_object_info', return_value=info) as GOF:
936
            r = self.client.get_object_sharing(obj)
937
            self.assertEqual(GOF.mock_calls[-1], call(obj))
938
            self.assert_dicts_are_equal(r, expected)
939
            info['x-object-sharing'] = '//'.join(
940
                ['%s=%s' % (k, v) for k, v in expected.items()])
941
            self.assertRaises(
942
                ValueError,
943
                self.client.get_object_sharing,
944
                obj)
945
            info['x-object-sharing'] = '; '.join(
946
                ['%s:%s' % (k, v) for k, v in expected.items()])
947
            self.assertRaises(
948
                ClientError,
949
                self.client.get_object_sharing,
950
                obj)
951
            info['x-object-sharing'] = 'read=%s' % expected['read']
952
            r = self.client.get_object_sharing(obj)
953
            expected.pop('write')
954
            self.assert_dicts_are_equal(r, expected)
955

    
956
    @patch('%s.object_post' % pithos_pkg, return_value=FR())
957
    def test_set_object_sharing(self, POST):
958
        read_perms = ['u1', 'g1', 'u2', 'g2']
959
        write_perms = ['u1', 'g1']
960
        for kwargs in (
961
                dict(read_permition=read_perms, write_permition=write_perms),
962
                dict(read_permition=read_perms),
963
                dict(write_permition=write_perms),
964
                dict()):
965
            self.client.set_object_sharing(obj, **kwargs)
966
            kwargs['read'] = kwargs.pop('read_permition', '')
967
            kwargs['write'] = kwargs.pop('write_permition', '')
968
            self.assertEqual(
969
                POST.mock_calls[-1],
970
                call(obj, update=True, permissions=kwargs))
971

    
972
    @patch('%s.set_object_sharing' % pithos_pkg)
973
    def test_del_object_sharing(self, SOS):
974
        self.client.del_object_sharing(obj)
975
        self.assertEqual(SOS.mock_calls[-1], call(obj))
976

    
977
    @patch('%s.get_container_info' % pithos_pkg, return_value=container_info)
978
    @patch('%s.object_post' % pithos_pkg, return_value=FR())
979
    def test_append_object(self, post, GCI):
980
        num_of_blocks = 4
981
        tmpFile = self._create_temp_file(num_of_blocks)
982
        tmpFile.seek(0, 2)
983
        file_size = tmpFile.tell()
984
        for turn in range(2):
985
            tmpFile.seek(0, 0)
986

    
987
            try:
988
                from progress.bar import ShadyBar
989
                apn_bar = ShadyBar('Mock append')
990
            except ImportError:
991
                apn_bar = None
992

    
993
            if apn_bar:
994

    
995
                def append_gen(n):
996
                    for i in apn_bar.iter(range(n)):
997
                        yield
998
                    yield
999

    
1000
            else:
1001
                append_gen = None
1002

    
1003
            self.client.append_object(
1004
                obj, tmpFile,
1005
                upload_cb=append_gen if turn else None)
1006
            self.assertEqual((turn + 1) * num_of_blocks, len(post.mock_calls))
1007
            (args, kwargs) = post.mock_calls[-1][1:3]
1008
            self.assertEqual(args, (obj,))
1009
            self.assertEqual(kwargs['content_length'], len(kwargs['data']))
1010
            fsize = num_of_blocks * int(kwargs['content_length'])
1011
            self.assertEqual(fsize, file_size)
1012
            self.assertEqual(kwargs['content_range'], 'bytes */*')
1013
            exp = 'application/octet-stream'
1014
            self.assertEqual(kwargs['content_type'], exp)
1015
            self.assertEqual(kwargs['update'], True)
1016

    
1017
    @patch('%s.object_post' % pithos_pkg, return_value=FR())
1018
    def test_truncate_object(self, post):
1019
        upto_bytes = 377
1020
        self.client.truncate_object(obj, upto_bytes)
1021
        self.assertEqual(post.mock_calls[-1], call(
1022
            obj,
1023
            update=True,
1024
            object_bytes=upto_bytes,
1025
            content_range='bytes 0-%s/*' % upto_bytes,
1026
            content_type='application/octet-stream',
1027
            source_object='/%s/%s' % (self.client.container, obj)))
1028

    
1029
    @patch('%s.get_container_info' % pithos_pkg, return_value=container_info)
1030
    @patch('%s.object_post' % pithos_pkg, return_value=FR())
1031
    def test_overwrite_object(self, post, GCI):
1032
        num_of_blocks = 4
1033
        tmpFile = self._create_temp_file(num_of_blocks)
1034
        tmpFile.seek(0, 2)
1035
        file_size = tmpFile.tell()
1036
        info = dict(object_info)
1037
        info['content-length'] = file_size
1038
        block_size = container_info['x-container-block-size']
1039
        with patch.object(PC, 'get_object_info', return_value=info) as GOI:
1040
            for start, end in (
1041
                    (0, file_size + 1),
1042
                    (file_size + 1, file_size + 2)):
1043
                tmpFile.seek(0, 0)
1044
                self.assertRaises(
1045
                    ClientError,
1046
                    self.client.overwrite_object,
1047
                    obj, start, end, tmpFile)
1048
            for start, end in ((0, 144), (144, 233), (233, file_size)):
1049
                tmpFile.seek(0, 0)
1050
                owr_gen = None
1051
                exp_size = end - start + 1
1052
                if not start or exp_size > block_size:
1053
                    try:
1054
                        from progress.bar import ShadyBar
1055
                        owr_bar = ShadyBar('Mock append')
1056
                    except ImportError:
1057
                        owr_bar = None
1058

    
1059
                    if owr_bar:
1060

    
1061
                        def owr_gen(n):
1062
                            for i in owr_bar.iter(range(n)):
1063
                                yield
1064
                            yield
1065

    
1066
                    if exp_size > block_size:
1067
                        exp_size = exp_size % block_size or block_size
1068

    
1069
                self.client.overwrite_object(obj, start, end, tmpFile, owr_gen)
1070
                self.assertEqual(GOI.mock_calls[-1], call(obj))
1071
                self.assertEqual(GCI.mock_calls[-1], call())
1072
                (args, kwargs) = post.mock_calls[-1][1:3]
1073
                self.assertEqual(args, (obj,))
1074
                self.assertEqual(len(kwargs['data']), exp_size)
1075
                self.assertEqual(kwargs['content_length'], exp_size)
1076
                self.assertEqual(kwargs['update'], True)
1077
                exp = 'application/octet-stream'
1078
                self.assertEqual(kwargs['content_type'], exp)
1079

    
1080
    @patch('%s.set_param' % client_pkg)
1081
    @patch('%s.get' % pithos_pkg, return_value=FR())
1082
    def test_get_sharing_accounts(self, get, SP):
1083
        FR.json = sharers
1084
        for kws in (
1085
                dict(),
1086
                dict(limit='50m3-11m17'),
1087
                dict(marker='X'),
1088
                dict(limit='50m3-11m17', marker='X')):
1089
            r = self.client.get_sharing_accounts(**kws)
1090
            self.assertEqual(SP.mock_calls[-3], call('format', 'json'))
1091
            limit, marker = kws.get('limit', None), kws.get('marker', None)
1092
            self.assertEqual(SP.mock_calls[-2], call(
1093
                'limit', limit,
1094
                iff=limit is not None))
1095
            self.assertEqual(SP.mock_calls[-1], call(
1096
                'marker', marker,
1097
                iff=marker is not None))
1098
            self.assertEqual(get.mock_calls[-1], call('', success=(200, 204)))
1099
            for i in range(len(r)):
1100
                self.assert_dicts_are_equal(r[i], sharers[i])
1101

    
1102
    @patch('%s.object_get' % pithos_pkg, return_value=FR())
1103
    def test_get_object_versionlist(self, get):
1104
        info = dict(object_info)
1105
        info['versions'] = ['v1', 'v2']
1106
        FR.json = info
1107
        r = self.client.get_object_versionlist(obj)
1108
        self.assertEqual(
1109
            get.mock_calls[-1],
1110
            call(obj, format='json', version='list'))
1111
        self.assertEqual(r, info['versions'])
1112

    
1113
if __name__ == '__main__':
1114
    from sys import argv
1115
    from kamaki.clients.test import runTestCase
1116
    runTestCase(Pithos, 'Pithos+ Client', argv[1:])