Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / test / __init__.py @ f53483d8

History | View | Annotate | Download (23.3 kB)

1
#!/usr/bin/env python
2
#coding=utf8
3

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

    
37
from urlparse import urlunsplit, urlsplit, urlparse
38
from xml.dom import minidom
39
from urllib import quote, unquote
40

    
41
from snf_django.utils.testing import with_settings, astakos_user
42

    
43
from pithos.api import settings as pithos_settings
44
from pithos.api.test.util import is_date, get_random_data, get_random_name
45
from pithos.backends.migrate import initialize_db
46

    
47
from synnefo.lib.services import get_service_path
48
from synnefo.lib import join_urls
49

    
50
from django.test import TestCase
51
from django.test.client import Client, MULTIPART_CONTENT, FakePayload
52
from django.test.simple import DjangoTestSuiteRunner
53
from django.conf import settings
54
from django.utils.http import urlencode
55
from django.db.backends.creation import TEST_DATABASE_PREFIX
56

    
57
import django.utils.simplejson as json
58

    
59
import random
60
import functools
61

    
62

    
63
pithos_test_settings = functools.partial(with_settings, pithos_settings)
64

    
65
DATE_FORMATS = ["%a %b %d %H:%M:%S %Y",
66
                "%A, %d-%b-%y %H:%M:%S GMT",
67
                "%a, %d %b %Y %H:%M:%S GMT"]
68

    
69
o_names = ['kate.jpg',
70
           'kate_beckinsale.jpg',
71
           'How To Win Friends And Influence People.pdf',
72
           'moms_birthday.jpg',
73
           'poodle_strut.mov',
74
           'Disturbed - Down With The Sickness.mp3',
75
           'army_of_darkness.avi',
76
           'the_mad.avi',
77
           'photos/animals/dogs/poodle.jpg',
78
           'photos/animals/dogs/terrier.jpg',
79
           'photos/animals/cats/persian.jpg',
80
           'photos/animals/cats/siamese.jpg',
81
           'photos/plants/fern.jpg',
82
           'photos/plants/rose.jpg',
83
           'photos/me.jpg']
84

    
85
details = {'container': ('name', 'count', 'bytes', 'last_modified',
86
                         'x_container_policy'),
87
           'object': ('name', 'hash', 'bytes', 'content_type',
88
                      'content_encoding', 'last_modified',)}
89

    
90
TEST_BLOCK_SIZE = 1024
91
TEST_HASH_ALGORITHM = 'sha256'
92

    
93
print 'backend module:', pithos_settings.BACKEND_DB_MODULE
94
print 'backend database engine:', settings.DATABASES['default']['ENGINE']
95
print 'update md5:', pithos_settings.UPDATE_MD5
96

    
97

    
98
django_sqlalchemy_engines = {
99
    'django.db.backends.postgresql_psycopg2': 'postgresql+psycopg2',
100
    'django.db.backends.postgresql': 'postgresql',
101
    'django.db.backends.mysql': '',
102
    'django.db.backends.sqlite3': 'mssql',
103
    'django.db.backends.oracle': 'oracle'}
104

    
105

    
106
def prepare_db_connection():
107
    """Build pithos backend connection string from django default database"""
108

    
109
    db = settings.DATABASES['default']
110
    name = db.get('TEST_NAME', TEST_DATABASE_PREFIX + db['NAME'])
111

    
112
    if (pithos_settings.BACKEND_DB_MODULE == 'pithos.backends.lib.sqlalchemy'):
113
        if db['ENGINE'] == 'django.db.backends.sqlite3':
114
            db_connection = 'sqlite:///%s' % name
115
        else:
116
            d = dict(scheme=django_sqlalchemy_engines.get(db['ENGINE']),
117
                     user=db['USER'],
118
                     pwd=db['PASSWORD'],
119
                     host=db['HOST'].lower(),
120
                     port=int(db['PORT']) if db['PORT'] != '' else '',
121
                     name=name)
122
            db_connection = (
123
                '%(scheme)s://%(user)s:%(pwd)s@%(host)s:%(port)s/%(name)s' % d)
124

    
125
            # initialize pithos database
126
            initialize_db(db_connection)
127
    else:
128
        db_connection = name
129
    pithos_settings.BACKEND_DB_CONNECTION = db_connection
130

    
131

    
132
def filter_headers(headers, prefix):
133
    meta = {}
134
    for k, v in headers.iteritems():
135
        if not k.startswith(prefix):
136
            continue
137
        meta[unquote(k[len(prefix):])] = unquote(v)
138
    return meta
139

    
140

    
141
class PithosTestSuiteRunner(DjangoTestSuiteRunner):
142
    def setup_databases(self, **kwargs):
143
        old_names, mirrors = super(PithosTestSuiteRunner,
144
                                   self).setup_databases(**kwargs)
145
        prepare_db_connection()
146
        return old_names, mirrors
147

    
148
    def teardown_databases(self, old_config, **kwargs):
149
        from pithos.api.util import _pithos_backend_pool
150
        _pithos_backend_pool.shutdown()
151
        super(PithosTestSuiteRunner, self).teardown_databases(old_config,
152
                                                              **kwargs)
153

    
154

    
155
class PithosTestClient(Client):
156
    def copy(self, path, data={}, content_type=MULTIPART_CONTENT,
157
             follow=False, **extra):
158
        """
159
        Send a resource to the server using COPY.
160
        """
161
        parsed = urlparse(path)
162
        r = {
163
            'CONTENT_TYPE':    'text/html; charset=utf-8',
164
            'PATH_INFO':       self._get_path(parsed),
165
            'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
166
            'REQUEST_METHOD': 'COPY',
167
            'wsgi.input':      FakePayload('')
168
        }
169
        r.update(extra)
170

    
171
        response = self.request(**r)
172
        if follow:
173
            response = self._handle_redirects(response, **extra)
174
        return response
175

    
176
    def move(self, path, data={}, content_type=MULTIPART_CONTENT,
177
             follow=False, **extra):
178
        """
179
        Send a resource to the server using MOVE.
180
        """
181
        parsed = urlparse(path)
182
        r = {
183
            'CONTENT_TYPE':    'text/html; charset=utf-8',
184
            'PATH_INFO':       self._get_path(parsed),
185
            'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
186
            'REQUEST_METHOD': 'MOVE',
187
            'wsgi.input':      FakePayload('')
188
        }
189
        r.update(extra)
190

    
191
        response = self.request(**r)
192
        if follow:
193
            response = self._handle_redirects(response, **extra)
194
        return response
195

    
196

    
197
class PithosAPITest(TestCase):
198
    def setUp(self):
199
        self.client = PithosTestClient()
200

    
201
        # Override default block size to spead up tests
202
        pithos_settings.BACKEND_BLOCK_SIZE = TEST_BLOCK_SIZE
203
        pithos_settings.BACKEND_HASH_ALGORITHM = TEST_HASH_ALGORITHM
204

    
205
        self.user = 'user'
206
        self.pithos_path = join_urls(get_service_path(
207
            pithos_settings.pithos_services, 'object-store'))
208

    
209
    def tearDown(self):
210
        #delete additionally created metadata
211
        meta = self.get_account_meta()
212
        self.delete_account_meta(meta)
213

    
214
        #delete additionally created groups
215
        groups = self.get_account_groups()
216
        self.delete_account_groups(groups)
217

    
218
        self._clean_account()
219

    
220
    def _clean_account(self):
221
        for c in self.list_containers():
222
            self.delete_container_content(c['name'])
223
            self.delete_container(c['name'])
224

    
225
    def head(self, url, user='user', token='DummyToken', data={}, follow=False,
226
             **extra):
227
        with astakos_user(user):
228
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
229
            if token:
230
                extra['HTTP_X_AUTH_TOKEN'] = token
231
            response = self.client.head(url, data, follow, **extra)
232
        return response
233

    
234
    def get(self, url, user='user', token='DummyToken', data={}, follow=False,
235
            **extra):
236
        with astakos_user(user):
237
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
238
            if token:
239
                extra['HTTP_X_AUTH_TOKEN'] = token
240
            response = self.client.get(url, data, follow, **extra)
241
        return response
242

    
243
    def delete(self, url, user='user', token='DummyToken', data={},
244
               follow=False, **extra):
245
        with astakos_user(user):
246
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
247
            if token:
248
                extra['HTTP_X_AUTH_TOKEN'] = token
249
            response = self.client.delete(url, data, follow, **extra)
250
        return response
251

    
252
    def post(self, url, user='user', token='DummyToken', data={},
253
             content_type='application/octet-stream', follow=False, **extra):
254
        with astakos_user(user):
255
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
256
            if token:
257
                extra['HTTP_X_AUTH_TOKEN'] = token
258
            response = self.client.post(url, data, content_type, follow,
259
                                        **extra)
260
        return response
261

    
262
    def put(self, url, user='user', token='DummyToken', data={},
263
            content_type='application/octet-stream', follow=False, **extra):
264
        with astakos_user(user):
265
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
266
            if token:
267
                extra['HTTP_X_AUTH_TOKEN'] = token
268
            response = self.client.put(url, data, content_type, follow,
269
                                       **extra)
270
        return response
271

    
272
    def copy(self, url, user='user', token='DummyToken', data={},
273
             content_type='application/octet-stream', follow=False, **extra):
274
        with astakos_user(user):
275
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
276
            if token:
277
                extra['HTTP_X_AUTH_TOKEN'] = token
278
            response = self.client.copy(url, data, content_type, follow,
279
                                        **extra)
280
        return response
281

    
282
    def move(self, url, user='user', token='DummyToken', data={},
283
             content_type='application/octet-stream', follow=False, **extra):
284
        with astakos_user(user):
285
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
286
            if token:
287
                extra['HTTP_X_AUTH_TOKEN'] = token
288
            response = self.client.move(url, data, content_type, follow,
289
                                        **extra)
290
        return response
291

    
292
    def update_account_meta(self, meta, user=None):
293
        user = user or self.user
294
        kwargs = dict(
295
            ('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items())
296
        url = join_urls(self.pithos_path, user)
297
        r = self.post('%s?update=' % url, user=user, **kwargs)
298
        self.assertEqual(r.status_code, 202)
299
        account_meta = self.get_account_meta(user=user)
300
        (self.assertTrue('X-Account-Meta-%s' % k in account_meta) for
301
            k in meta.keys())
302
        (self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for
303
            k, v in meta.items())
304

    
305
    def reset_account_meta(self, meta, user=None):
306
        user = user or self.user
307
        kwargs = dict(
308
            ('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items())
309
        url = join_urls(self.pithos_path, user)
310
        r = self.post(url, user=user, **kwargs)
311
        self.assertEqual(r.status_code, 202)
312
        account_meta = self.get_account_meta(user=user)
313
        (self.assertTrue('X-Account-Meta-%s' % k in account_meta) for
314
            k in meta.keys())
315
        (self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for
316
            k, v in meta.items())
317

    
318
    def delete_account_meta(self, meta, user=None):
319
        user = user or self.user
320
        transform = lambda k: 'HTTP_%s' % k.replace('-', '_').upper()
321
        kwargs = dict((transform(k), '') for k, v in meta.items())
322
        url = join_urls(self.pithos_path, user)
323
        r = self.post('%s?update=' % url, user=user, **kwargs)
324
        self.assertEqual(r.status_code, 202)
325
        account_meta = self.get_account_meta(user=user)
326
        (self.assertTrue('X-Account-Meta-%s' % k not in account_meta) for
327
            k in meta.keys())
328
        return r
329

    
330
    def delete_account_groups(self, groups, user=None):
331
        user = user or self.user
332
        url = join_urls(self.pithos_path, user)
333
        r = self.post('%s?update=' % url, user=user, **groups)
334
        self.assertEqual(r.status_code, 202)
335
        account_groups = self.get_account_groups()
336
        (self.assertTrue(k not in account_groups) for k in groups.keys())
337
        return r
338

    
339
    def get_account_info(self, until=None, user=None):
340
        user = user or self.user
341
        url = join_urls(self.pithos_path, user)
342
        if until is not None:
343
            parts = list(urlsplit(url))
344
            parts[3] = urlencode({
345
                'until': until
346
            })
347
            url = urlunsplit(parts)
348
        r = self.head(url, user=user)
349
        self.assertEqual(r.status_code, 204)
350
        return r
351

    
352
    def get_account_meta(self, until=None, user=None):
353
        prefix = 'X-Account-Meta-'
354
        r = self.get_account_info(until=until, user=user)
355
        headers = dict(r._headers.values())
356
        return filter_headers(headers, prefix)
357

    
358
    def get_account_groups(self, until=None, user=None):
359
        prefix = 'X-Account-Group-'
360
        r = self.get_account_info(until=until, user=user)
361
        headers = dict(r._headers.values())
362
        return filter_headers(headers, prefix)
363

    
364
    def get_container_info(self, container, until=None, user=None):
365
        user = user or self.user
366
        url = join_urls(self.pithos_path, user, container)
367
        if until is not None:
368
            parts = list(urlsplit(url))
369
            parts[3] = urlencode({
370
                'until': until
371
            })
372
            url = urlunsplit(parts)
373
        r = self.head(url, user=user)
374
        self.assertEqual(r.status_code, 204)
375
        return r
376

    
377
    def get_container_meta(self, container, until=None, user=None):
378
        prefix = 'X-Container-Meta-'
379
        r = self.get_container_info(container, until=until, user=user)
380
        headers = dict(r._headers.values())
381
        return filter_headers(headers, prefix)
382

    
383
    def update_container_meta(self, container, meta, user=None):
384
        user = user or self.user
385
        kwargs = dict(
386
            ('HTTP_X_CONTAINER_META_%s' % k, str(v)) for k, v in meta.items())
387
        url = join_urls(self.pithos_path, user, container)
388
        r = self.post('%s?update=' % url, user=user, **kwargs)
389
        self.assertEqual(r.status_code, 202)
390
        container_meta = self.get_container_meta(container, user=user)
391
        (self.assertTrue('X-Container-Meta-%s' % k in container_meta) for
392
            k in meta.keys())
393
        (self.assertEqual(container_meta['X-Container-Meta-%s' % k], v) for
394
            k, v in meta.items())
395

    
396
    def list_containers(self, format='json', headers={}, user=None, **params):
397
        user = user or self.user
398
        _url = join_urls(self.pithos_path, user)
399
        parts = list(urlsplit(_url))
400
        params['format'] = format
401
        parts[3] = urlencode(params)
402
        url = urlunsplit(parts)
403
        _headers = dict(('HTTP_%s' % k.upper(), str(v))
404
                        for k, v in headers.items())
405
        r = self.get(url, user=user, **_headers)
406

    
407
        if format is None:
408
            containers = r.content.split('\n')
409
            if '' in containers:
410
                containers.remove('')
411
            return containers
412
        elif format == 'json':
413
            try:
414
                containers = json.loads(r.content)
415
            except:
416
                self.fail('json format expected')
417
            return containers
418
        elif format == 'xml':
419
            return minidom.parseString(r.content)
420

    
421
    def delete_container_content(self, cname, user=None):
422
        user = user or self.user
423
        url = join_urls(self.pithos_path, user, cname)
424
        r = self.delete('%s?delimiter=/' % url, user=user)
425
        self.assertEqual(r.status_code, 204)
426
        return r
427

    
428
    def delete_container(self, cname, user=None):
429
        user = user or self.user
430
        url = join_urls(self.pithos_path, user, cname)
431
        r = self.delete(url, user=user)
432
        self.assertEqual(r.status_code, 204)
433
        return r
434

    
435
    def create_container(self, cname=None, user=None):
436
        cname = cname or get_random_name()
437
        user = user or self.user
438
        url = join_urls(self.pithos_path, user, cname)
439
        r = self.put(url, user=user, data='')
440
        self.assertTrue(r.status_code in (202, 201))
441
        return cname, r
442

    
443
    def upload_object(self, cname, oname=None, length=None, verify=True,
444
                      user=None, **meta):
445
        oname = oname or get_random_name()
446
        length = length or random.randint(TEST_BLOCK_SIZE, 2 * TEST_BLOCK_SIZE)
447
        user = user or self.user
448
        data = get_random_data(length=length)
449
        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
450
                       for k, v in meta.iteritems())
451
        url = join_urls(self.pithos_path, user, cname, oname)
452
        r = self.put(url, user=user, data=data, **headers)
453
        if verify:
454
            self.assertEqual(r.status_code, 201)
455
        return oname, data, r
456

    
457
    def update_object_data(self, cname, oname=None, length=None,
458
                           content_type=None, content_range=None,
459
                           verify=True, user=None, **meta):
460
        oname = oname or get_random_name()
461
        length = length or random.randint(TEST_BLOCK_SIZE, 2 * TEST_BLOCK_SIZE)
462
        content_type = content_type or 'application/octet-stream'
463
        user = user or self.user
464
        data = get_random_data(length=length)
465
        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
466
                       for k, v in meta.iteritems())
467
        if content_range:
468
            headers['HTTP_CONTENT_RANGE'] = content_range
469
        url = join_urls(self.pithos_path, user, cname, oname)
470
        r = self.post(url, user=user, data=data, content_type=content_type,
471
                      **headers)
472
        if verify:
473
            self.assertEqual(r.status_code, 204)
474
        return oname, data, r
475

    
476
    def append_object_data(self, cname, oname=None, length=None,
477
                           content_type=None, user=None):
478
        return self.update_object_data(cname, oname=oname,
479
                                       length=length,
480
                                       content_type=content_type,
481
                                       content_range='bytes */*',
482
                                       user=user)
483

    
484
    def create_folder(self, cname, oname=None, user=None, **headers):
485
        user = user or self.user
486
        oname = oname or get_random_name()
487
        url = join_urls(self.pithos_path, user, cname, oname)
488
        r = self.put(url, user=user, data='',
489
                     content_type='application/directory', **headers)
490
        self.assertEqual(r.status_code, 201)
491
        return oname, r
492

    
493
    def list_objects(self, cname, prefix=None, user=None):
494
        user = user or self.user
495
        url = join_urls(self.pithos_path, user, cname)
496
        path = '%s?format=json' % url
497
        if prefix is not None:
498
            path = '%s&prefix=%s' % (path, prefix)
499
        r = self.get(path, user=user)
500
        self.assertTrue(r.status_code in (200, 204))
501
        try:
502
            objects = json.loads(r.content)
503
        except:
504
            self.fail('json format expected')
505
        return objects
506

    
507
    def get_object_info(self, container, object, version=None, until=None,
508
                        user=None):
509
        user = user or self.user
510
        url = join_urls(self.pithos_path, user, container, object)
511
        if until is not None:
512
            parts = list(urlsplit(url))
513
            parts[3] = urlencode({
514
                'until': until
515
            })
516
            url = urlunsplit(parts)
517
        if version:
518
            url = '%s?version=%s' % (url, version)
519
        r = self.head(url, user=user)
520
        self.assertEqual(r.status_code, 200)
521
        return r
522

    
523
    def get_object_meta(self, container, object, version=None, until=None,
524
                        user=None):
525
        prefix = 'X-Object-Meta-'
526
        user = user or self.user
527
        r = self.get_object_info(container, object, version, until=until,
528
                                 user=user)
529
        headers = dict(r._headers.values())
530
        return filter_headers(headers, prefix)
531

    
532
    def update_object_meta(self, container, object, meta, user=None):
533
        user = user or self.user
534
        kwargs = dict(
535
            ('HTTP_X_OBJECT_META_%s' % k, str(v)) for k, v in meta.items())
536
        url = join_urls(self.pithos_path, user, container, object)
537
        r = self.post('%s?update=' % url, user=user, content_type='', **kwargs)
538
        self.assertEqual(r.status_code, 202)
539
        object_meta = self.get_object_meta(container, object, user=user)
540
        (self.assertTrue('X-Objecr-Meta-%s' % k in object_meta) for
541
            k in meta.keys())
542
        (self.assertEqual(object_meta['X-Object-Meta-%s' % k], v) for
543
            k, v in meta.items())
544

    
545
    def assert_extended(self, data, format, type, size=10000):
546
        if format == 'xml':
547
            self._assert_xml(data, type, size)
548
        elif format == 'json':
549
            self._assert_json(data, type, size)
550

    
551
    def _assert_json(self, data, type, size):
552
        convert = lambda s: s.lower()
553
        info = [convert(elem) for elem in details[type]]
554
        self.assertTrue(len(data) <= size)
555
        for item in info:
556
            for i in data:
557
                if 'subdir' in i.keys():
558
                    continue
559
                self.assertTrue(item in i.keys())
560

    
561
    def _assert_xml(self, data, type, size):
562
        convert = lambda s: s.lower()
563
        info = [convert(elem) for elem in details[type]]
564
        try:
565
            info.remove('content_encoding')
566
        except ValueError:
567
            pass
568
        xml = data
569
        entities = xml.getElementsByTagName(type)
570
        self.assertTrue(len(entities) <= size)
571
        for e in entities:
572
            for item in info:
573
                self.assertTrue(e.getElementsByTagName(item))
574

    
575

    
576
class AssertMappingInvariant(object):
577
    def __init__(self, callable, *args, **kwargs):
578
        self.callable = callable
579
        self.args = args
580
        self.kwargs = kwargs
581

    
582
    def __enter__(self):
583
        self.map = self.callable(*self.args, **self.kwargs)
584
        return self.map
585

    
586
    def __exit__(self, type, value, tb):
587
        map = self.callable(*self.args, **self.kwargs)
588
        for k, v in self.map.items():
589
            if is_date(v):
590
                continue
591

    
592
            assert(k in map), '%s not in map' % k
593
            assert v == map[k]
594

    
595

    
596
class AssertUUidInvariant(object):
597
    def __init__(self, callable, *args, **kwargs):
598
        self.callable = callable
599
        self.args = args
600
        self.kwargs = kwargs
601

    
602
    def __enter__(self):
603
        self.map = self.callable(*self.args, **self.kwargs)
604
        assert('x-object-uuid' in self.map)
605
        self.uuid = self.map['x-object-uuid']
606
        return self.map
607

    
608
    def __exit__(self, type, value, tb):
609
        map = self.callable(*self.args, **self.kwargs)
610
        assert('x-object-uuid' in self.map)
611
        uuid = map['x-object-uuid']
612
        assert(uuid == self.uuid)