Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / test.py @ 9dc724e5

History | View | Annotate | Download (20 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 mock import patch, call
35
from unittest import makeSuite, TestSuite, TextTestRunner, TestCase
36
from time import sleep
37
from inspect import getmembers, isclass
38
from itertools import product
39
from random import randint
40

    
41
from kamaki.clients.utils.test import Utils
42
from kamaki.clients.astakos.test import AstakosClient
43
from kamaki.clients.compute.test import ComputeClient, ComputeRestClient
44
from kamaki.clients.cyclades.test import CycladesClient
45
from kamaki.clients.cyclades.test import CycladesRestClient
46
from kamaki.clients.image.test import ImageClient
47
from kamaki.clients.storage.test import StorageClient
48
from kamaki.clients.pithos.test import PithosClient, PithosRestClient
49

    
50

    
51
class ClientError(TestCase):
52

    
53
    def test___init__(self):
54
        from kamaki.clients import ClientError
55
        for msg, status, details, exp_msg, exp_status, exp_details in (
56
                ('some msg', 42, 0.28, 0, 0, 0),
57
                ('some msg', 'fail', [], 0, 0, 0),
58
                ('some msg', 42, 'details on error', 0, 0, 0),
59
                (
60
                    '404 {"ExampleError":'
61
                    ' {"message": "a msg", "code": 42, "details": "dets"}}',
62
                    404,
63
                    0,
64
                    '404 ExampleError (a msg)\n',
65
                    42,
66
                    ['dets']),
67
                (
68
                    '404 {"ExampleError":'
69
                    ' {"message": "a msg", "code": 42}}',
70
                    404,
71
                    'details on error',
72
                    '404 ExampleError (a msg)\n',
73
                    42,
74
                    0),
75
                (
76
                    '404 {"ExampleError":'
77
                    ' {"details": "Explain your error"}}',
78
                    404,
79
                    'details on error',
80
                    '404 ExampleError',
81
                    0,
82
                    ['details on error', 'Explain your error']),
83
                ('some msg\n', -10, ['details', 'on', 'error'], 0, 0, 0)):
84
            ce = ClientError(msg, status, details)
85
            exp_msg = exp_msg or (msg if msg.endswith('\n') else msg + '\n')
86
            exp_status = exp_status or status
87
            exp_details = exp_details or details
88
            self.assertEqual('%s' % ce, exp_msg)
89
            self.assertEqual(
90
                exp_status if isinstance(exp_status, int) else 0,
91
                ce.status)
92
            self.assertEqual(exp_details, ce.details)
93

    
94

    
95
class RequestManager(TestCase):
96

    
97
    def setUp(self):
98
        from kamaki.clients import RequestManager
99
        self.RM = RequestManager
100

    
101
    def test___init__(self):
102
        from kamaki.clients import HTTP_METHODS
103
        method_values = HTTP_METHODS + [v.lower() for v in HTTP_METHODS]
104
        for args in product(
105
                tuple(method_values),
106
                ('http://www.example.com', 'https://example.com', ''),
107
                ('/some/path', '/' ''),
108
                ('Some data', '', None),
109
                (dict(k1='v1', k2='v2'), dict()),
110
                (dict(k='v', k2=None, k3='v3'), dict(k=0), dict(k='v'), {})):
111
            req = self.RM(*args)
112
            method, url, path, data, headers, params = args
113
            self.assertEqual(req.method, method.upper())
114
            for i, (k, v) in enumerate(params.items()):
115
                path += '%s%s%s' % (
116
                    '&' if '?' in path or i else '?',
117
                    k,
118
                    ('=%s' % v) if v else '')
119
            self.assertEqual(req.path, path)
120
            self.assertEqual(req.data, data)
121
            self.assertEqual(req.headers, headers)
122
        self.assertRaises(AssertionError, self.RM, 'GOT', '', '', '', {}, {})
123

    
124
    @patch('httplib.HTTPConnection.getresponse')
125
    @patch('httplib.HTTPConnection.request')
126
    def test_perform(self, request, getresponse):
127
        from httplib import HTTPConnection
128
        self.RM('GET', 'http://example.com', '/').perform(
129
            HTTPConnection('http', 'example.com'))
130
        expected = dict(body=None, headers={}, url='/', method='GET')
131
        request.assert_called_once_with(**expected)
132
        getresponse.assert_called_once_with()
133

    
134

    
135
class FakeResp(object):
136

    
137
    READ = 'something to read'
138
    HEADERS = dict(k='v', k1='v1', k2='v2')
139
    reason = 'some reason'
140
    status = 42
141

    
142
    def read(self):
143
        return self.READ
144

    
145
    def getheaders(self):
146
        return self.HEADERS.items()
147

    
148

    
149
class ResponseManager(TestCase):
150

    
151
    def setUp(self):
152
        from kamaki.clients import ResponseManager, RequestManager
153
        from httplib import HTTPConnection
154
        self.RM = ResponseManager(RequestManager('GET', 'http://ok', '/'))
155
        self.HTTPC = HTTPConnection
156

    
157
    def tearDown(self):
158
        FakeResp.READ = 'something to read'
159

    
160
    @patch('kamaki.clients.RequestManager.perform', return_value=FakeResp())
161
    def test_content(self, perform):
162
        self.assertEqual(self.RM.content, FakeResp.READ)
163
        self.assertTrue(isinstance(perform.call_args[0][0], self.HTTPC))
164

    
165
    @patch('kamaki.clients.RequestManager.perform', return_value=FakeResp())
166
    def test_text(self, perform):
167
        self.assertEqual(self.RM.text, FakeResp.READ)
168
        self.assertTrue(isinstance(perform.call_args[0][0], self.HTTPC))
169

    
170
    @patch('kamaki.clients.RequestManager.perform', return_value=FakeResp())
171
    def test_status(self, perform):
172
        self.assertEqual(self.RM.status, FakeResp.reason)
173
        self.assertTrue(isinstance(perform.call_args[0][0], self.HTTPC))
174

    
175
    @patch('kamaki.clients.RequestManager.perform', return_value=FakeResp())
176
    def test_status_code(self, perform):
177
        self.assertEqual(self.RM.status_code, FakeResp.status)
178
        self.assertTrue(isinstance(perform.call_args[0][0], self.HTTPC))
179

    
180
    @patch('kamaki.clients.RequestManager.perform', return_value=FakeResp())
181
    def test_headers(self, perform):
182
        self.assertEqual(self.RM.headers, FakeResp.HEADERS)
183
        self.assertTrue(isinstance(perform.call_args[0][0], self.HTTPC))
184

    
185
    @patch('kamaki.clients.RequestManager.perform', return_value=FakeResp())
186
    def test_json(self, perform):
187
        try:
188
            self.RM.json
189
        except Exception as e:
190
            self.assertEqual(
191
                '%s' % e,
192
                'Response not formated in JSON - '
193
                'No JSON object could be decoded\n')
194

    
195
        from json import dumps
196
        FakeResp.READ = dumps(FakeResp.HEADERS)
197
        self.RM._request_performed = False
198
        self.assertEqual(self.RM.json, FakeResp.HEADERS)
199
        self.assertTrue(isinstance(perform.call_args[0][0], self.HTTPC))
200

    
201
    @patch('kamaki.clients.RequestManager.perform', return_value=FakeResp())
202
    def test_all(self, perform):
203
        self.assertEqual(self.RM.content, FakeResp.READ)
204
        self.assertEqual(self.RM.text, FakeResp.READ)
205
        self.assertEqual(self.RM.status, FakeResp.reason)
206
        self.assertEqual(self.RM.status_code, FakeResp.status)
207
        self.assertEqual(self.RM.headers, FakeResp.HEADERS)
208
        perform.assert_called_only_once
209

    
210

    
211
class SilentEvent(TestCase):
212

    
213
    def thread_content(self, methodid, raiseException=0):
214
        wait = 0.1
215
        self.can_finish = -1
216
        while self.can_finish < methodid and wait < 4:
217
            sleep(wait)
218
            wait = 2 * wait
219
        if raiseException and raiseException == methodid:
220
            raise Exception('Some exception')
221
        self._value = methodid
222
        self.assertTrue(wait < 4)
223

    
224
    def setUp(self):
225
        from kamaki.clients import SilentEvent
226
        self.SE = SilentEvent
227

    
228
    def test_run(self):
229
        threads = [self.SE(self.thread_content, i) for i in range(4)]
230
        for t in threads:
231
            t.start()
232

    
233
        for i in range(4):
234
            self.assertTrue(threads[i].is_alive())
235
            self.can_finish = i
236
            threads[i].join()
237
            self.assertFalse(threads[i].is_alive())
238

    
239
    def test_value(self):
240
        threads = [self.SE(self.thread_content, i) for i in range(4)]
241
        for t in threads:
242
            t.start()
243

    
244
        for mid, t in enumerate(threads):
245
            if t.is_alive():
246
                self.can_finish = mid
247
                continue
248
            self.assertTrue(mid, t.value)
249

    
250
    def test_exception(self):
251
        threads = [self.SE(self.thread_content, i, (i % 2)) for i in range(4)]
252
        for t in threads:
253
            t.start()
254

    
255
        for i, t in enumerate(threads):
256
            if t.is_alive():
257
                self.can_finish = i
258
                continue
259
            if i % 2:
260
                self.assertTrue(isinstance(t.exception, Exception))
261
            else:
262
                self.assertFalse(t.exception)
263

    
264

    
265
class FR(object):
266
    json = None
267
    text = None
268
    headers = dict()
269
    content = json
270
    status = None
271
    status_code = 200
272

    
273

    
274
class FakeConnection(object):
275
    """A fake Connection class"""
276

    
277
    headers = dict()
278
    params = dict()
279

    
280
    def __init__(self):
281
        pass
282

    
283
    def set_header(self, name, value):
284
        pass
285

    
286
    def reset_headers(self):
287
        self.headers = {}
288

    
289
    def set_param(self, name, value):
290
        self.params = {}
291

    
292
    def reset_params(self):
293
        pass
294

    
295
    def perform_request(self, *args):
296
        return FR()
297

    
298

    
299
class Client(TestCase):
300

    
301
    def assert_dicts_are_equal(self, d1, d2):
302
        for k, v in d1.items():
303
            self.assertTrue(k in d2)
304
            if isinstance(v, dict):
305
                self.assert_dicts_are_equal(v, d2[k])
306
            else:
307
                self.assertEqual(unicode(v), unicode(d2[k]))
308

    
309
    def setUp(self):
310
        from kamaki.clients import Client
311
        from kamaki.clients import ClientError as CE
312
        self.base_url = 'http://example.com'
313
        self.token = 's0m370k3n=='
314
        self.client = Client(self.base_url, self.token, FakeConnection())
315
        self.CE = CE
316

    
317
    def tearDown(self):
318
        FR.text = None
319
        FR.status = None
320
        FR.status_code = 200
321
        FakeConnection.headers = dict()
322
        self.client.token = self.token
323

    
324
    def test___init__(self):
325
        self.assertEqual(self.client.base_url, self.base_url)
326
        self.assertEqual(self.client.token, self.token)
327
        self.assert_dicts_are_equal(self.client.headers, {})
328
        DATE_FORMATS = [
329
            '%a %b %d %H:%M:%S %Y',
330
            '%A, %d-%b-%y %H:%M:%S GMT',
331
            '%a, %d %b %Y %H:%M:%S GMT']
332
        self.assertEqual(self.client.DATE_FORMATS, DATE_FORMATS)
333
        self.assertTrue(isinstance(self.client.http_client, FakeConnection))
334

    
335
    def test__init_thread_limit(self):
336
        exp = 'Nothing set here'
337
        for faulty in (-1, 0.5, 'a string', {}):
338
            self.assertRaises(
339
                AssertionError,
340
                self.client._init_thread_limit,
341
                faulty)
342
            self.assertEqual(exp, getattr(self.client, '_thread_limit', exp))
343
            self.assertEqual(exp, getattr(self.client, '_elapsed_old', exp))
344
            self.assertEqual(exp, getattr(self.client, '_elapsed_new', exp))
345
        self.client._init_thread_limit(42)
346
        self.assertEqual(42, self.client._thread_limit)
347
        self.assertEqual(0.0, self.client._elapsed_old)
348
        self.assertEqual(0.0, self.client._elapsed_new)
349

    
350
    def test__watch_thread_limit(self):
351
        waits = (
352
            dict(args=((0.1, 1), (0.1, 2), (0.2, 1), (0.7, 1), (0.3, 2))),
353
            dict(args=((1.0 - (i / 10.0), (i + 1)) for i in range(7))),
354
            dict(max=1, args=tuple([(randint(1, 10) / 3.0, 1), ] * 10)),
355
            dict(
356
                limit=5,
357
                args=tuple([
358
                    (1.0 + (i / 10.0), (5 - i - 1)) for i in range(4)] + [
359
                    (2.0, 1), (1.9, 2), (2.0, 1), (2.0, 2)])),
360
            dict(args=tuple(
361
                [(1.0 - (i / 10.0), (i + 1)) for i in range(7)] + [
362
                (0.1, 7), (0.2, 6), (0.4, 5), (0.3, 6), (0.2, 7), (0.1, 7)])),)
363
        for wait_dict in waits:
364
            if 'max' in wait_dict:
365
                self.client.MAX_THREADS = wait_dict['max']
366
            else:
367
                self.client.MAX_THREADS = 7
368
            if 'limit' in wait_dict:
369
                self.client._init_thread_limit(wait_dict['limit'])
370
            else:
371
                self.client._init_thread_limit()
372
                self.client._watch_thread_limit(list())
373
                self.assertEqual(1, self.client._thread_limit)
374
            for wait, exp_limit in wait_dict['args']:
375
                self.client._elapsed_new = wait
376
                self.client._watch_thread_limit(list())
377
                self.assertEqual(exp_limit, self.client._thread_limit)
378

    
379
    def test__raise_for_status(self):
380
        r = FR()
381
        for txt, sts_code, sts in (('err msg', 10, None), ('', 42, 'Err St')):
382
            r.text, r.status_code, r.status = txt, sts_code, sts
383
            try:
384
                self.client._raise_for_status(r)
385
            except self.CE as ce:
386
                self.assertEqual('%s' % ce, '%s %s\n' % (sts or '', txt))
387
                self.assertEqual(ce.status, sts_code)
388

    
389
        for msg, sts_code in (('err msg', 32), ('', 42), ('an err', None)):
390
            err = self.CE(msg, sts_code) if sts_code else Exception(msg)
391
            try:
392
                self.client._raise_for_status(err)
393
            except self.CE as ce:
394
                self.assertEqual('%s' % ce, '%s %s\n' % (sts_code or '', msg))
395
                self.assertEqual(ce.status, sts_code or 0)
396

    
397
    @patch('%s.FakeConnection.set_header' % __name__)
398
    def test_set_header(self, SH):
399
        num_of_calls = 0
400
        for name, value, condition in product(
401
                ('n4m3', '', None),
402
                ('v41u3', None, 42),
403
                (True, False, None, 1, '')):
404
            self.client.set_header(name, value, iff=condition)
405
            if value is not None and condition:
406
                self.assertEqual(SH.mock_calls[-1], call(name, value))
407
                num_of_calls += 1
408
            else:
409
                self.assertEqual(num_of_calls, len(SH.mock_calls))
410

    
411
    @patch('%s.FakeConnection.set_param' % __name__)
412
    def test_set_param(self, SP):
413
        num_of_calls = 0
414
        for name, value, condition in product(
415
                ('n4m3', '', None),
416
                ('v41u3', None, 42),
417
                (True, False, None, 1, '')):
418
            self.client.set_param(name, value, iff=condition)
419
            if condition:
420
                self.assertEqual(SP.mock_calls[-1], call(name, value))
421
                num_of_calls += 1
422
            else:
423
                self.assertEqual(num_of_calls, len(SP.mock_calls))
424

    
425
    @patch('%s.FakeConnection.perform_request' % __name__, return_value=FR())
426
    def test_request(self, PR):
427
        for args in product(
428
                ('get', '', dict(method='get')),
429
                ('/some/path', None, ['some', 'path']),
430
                (dict(), dict(h1='v1'), dict(h1='v2', h2='v2')),
431
                (dict(), dict(p1='v1'), dict(p1='v2', p2=None, p3='v3')),
432
                (dict(), dict(data='some data'), dict(
433
                    success=400,
434
                    json=dict(k2='v2', k1='v1')))):
435
            method, path, kwargs = args[0], args[1], args[-1]
436
            args = args[:-1]
437
            if not (isinstance(method, str) and method and isinstance(
438
                    path, str) and path):
439
                self.assertRaises(
440
                    AssertionError,
441
                    self.client.request,
442
                    *args, **kwargs)
443
            else:
444
                atoken = 'a70k3n_%s' % randint(1, 30)
445
                self.client.token = atoken
446
                if 'success' in kwargs:
447
                    self.assertRaises(
448
                        self.CE,
449
                        self.client.request,
450
                        *args, **kwargs)
451
                    FR.status_code = kwargs['success']
452
                else:
453
                    FR.status_code = 200
454
                self.client.request(*args, **kwargs)
455
                data = kwargs.get(
456
                    'data',
457
                    '{"k2": "v2", "k1": "v1"}' if 'json' in kwargs else None)
458
                self.assertEqual(self.client.http_client.url, self.base_url)
459
                self.assertEqual(self.client.http_client.path, path)
460
                self.assertEqual(
461
                    PR.mock_calls[-1],
462
                    call(method, data, *args[2:]))
463
                self.assertEqual(self.client.http_client.headers, dict())
464
                self.assertEqual(self.client.http_client.params, dict())
465

    
466
    @patch('kamaki.clients.Client.request', return_value='lala')
467
    def _test_foo(self, foo, request):
468
        method = getattr(self.client, foo)
469
        r = method('path', k='v')
470
        self.assertEqual(r, 'lala')
471
        request.assert_called_once_with(foo, 'path', k='v')
472

    
473
    def test_delete(self):
474
        self._test_foo('delete')
475

    
476
    def test_get(self):
477
        self._test_foo('get')
478

    
479
    def test_head(self):
480
        self._test_foo('head')
481

    
482
    def test_post(self):
483
        self._test_foo('post')
484

    
485
    def test_put(self):
486
        self._test_foo('put')
487

    
488
    def test_copy(self):
489
        self._test_foo('copy')
490

    
491
    def test_move(self):
492
        self._test_foo('move')
493

    
494

    
495
#  TestCase auxiliary methods
496

    
497
def runTestCase(cls, test_name, args=[], failure_collector=[]):
498
    """
499
    :param cls: (TestCase) a set of Tests
500

501
    :param test_name: (str)
502

503
    :param args: (list) these are prefixed with test_ and used as params when
504
        instantiating cls
505

506
    :param failure_collector: (list) collects info of test failures
507

508
    :returns: (int) total # of run tests
509
    """
510
    suite = TestSuite()
511
    if args:
512
        suite.addTest(cls('_'.join(['test'] + args)))
513
    else:
514
        suite.addTest(makeSuite(cls))
515
    print('* Test * %s *' % test_name)
516
    r = TextTestRunner(verbosity=2).run(suite)
517
    failure_collector += r.failures
518
    return r.testsRun
519

    
520

    
521
def _add_value(foo, value):
522
    def wrap(self):
523
        return foo(self, value)
524
    return wrap
525

    
526

    
527
def get_test_classes(module=__import__(__name__), name=''):
528
    module_stack = [module]
529
    while module_stack:
530
        module = module_stack[-1]
531
        module_stack = module_stack[:-1]
532
        for objname, obj in getmembers(module):
533
            if (objname == name or not name):
534
                if isclass(obj) and objname != 'TestCase' and (
535
                        issubclass(obj, TestCase)):
536
                    yield (obj, objname)
537

    
538

    
539
def main(argv):
540
    found = False
541
    failure_collector = list()
542
    num_of_tests = 0
543
    for cls, name in get_test_classes(name=argv[1] if len(argv) > 1 else ''):
544
        found = True
545
        num_of_tests += runTestCase(cls, name, argv[2:], failure_collector)
546
    if not found:
547
        print('Test "%s" not found' % ' '.join(argv[1:]))
548
    else:
549
        for i, failure in enumerate(failure_collector):
550
            print('Failure %s: ' % (i + 1))
551
            for field in failure:
552
                print('\t%s' % field)
553
        print('\nTotal tests run: %s' % num_of_tests)
554
        print('Total failures: %s' % len(failure_collector))
555

    
556

    
557
if __name__ == '__main__':
558
    from sys import argv
559
    main(argv)