Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / test.py @ 6a6175c0

History | View | Annotate | Download (15.7 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.connection.test import (
42
    KamakiConnection,
43
    KamakiHTTPConnection,
44
    KamakiResponse,
45
    KamakiHTTPResponse)
46
from kamaki.clients.utils.test import Utils
47
from kamaki.clients.astakos.test import Astakos
48
from kamaki.clients.compute.test import Compute, ComputeRest
49
from kamaki.clients.cyclades.test import Cyclades, CycladesRest
50
from kamaki.clients.image.test import Image
51
from kamaki.clients.storage.test import Storage
52
from kamaki.clients.pithos.test import Pithos, PithosRest
53

    
54

    
55
class ClientError(TestCase):
56

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

    
98

    
99
class SilentEvent(TestCase):
100

    
101
    def thread_content(self, methodid, raiseException=0):
102
        wait = 0.1
103
        self.can_finish = -1
104
        while self.can_finish < methodid and wait < 4:
105
            sleep(wait)
106
            wait = 2 * wait
107
        if raiseException and raiseException == methodid:
108
            raise Exception('Some exception')
109
        self._value = methodid
110
        self.assertTrue(wait < 4)
111

    
112
    def setUp(self):
113
        from kamaki.clients import SilentEvent
114
        self.SE = SilentEvent
115

    
116
    def test_run(self):
117
        threads = [self.SE(self.thread_content, i) for i in range(4)]
118
        for t in threads:
119
            t.start()
120

    
121
        for i in range(4):
122
            self.assertTrue(threads[i].is_alive())
123
            self.can_finish = i
124
            threads[i].join()
125
            self.assertFalse(threads[i].is_alive())
126

    
127
    def test_value(self):
128
        threads = [self.SE(self.thread_content, i) for i in range(4)]
129
        for t in threads:
130
            t.start()
131

    
132
        for mid, t in enumerate(threads):
133
            if t.is_alive():
134
                self.can_finish = mid
135
                continue
136
            self.assertTrue(mid, t.value)
137

    
138
    def test_exception(self):
139
        threads = [self.SE(self.thread_content, i, (i % 2)) for i in range(4)]
140
        for t in threads:
141
            t.start()
142

    
143
        for i, t in enumerate(threads):
144
            if t.is_alive():
145
                self.can_finish = i
146
                continue
147
            if i % 2:
148
                self.assertTrue(isinstance(t.exception, Exception))
149
            else:
150
                self.assertFalse(t.exception)
151

    
152

    
153
class FR(object):
154
    json = None
155
    text = None
156
    headers = dict()
157
    content = json
158
    status = None
159
    status_code = 200
160

    
161
    def release(self):
162
        pass
163

    
164

    
165
class FakeConnection(object):
166
    """A fake Connection class"""
167

    
168
    headers = dict()
169
    params = dict()
170

    
171
    def __init__(self):
172
        pass
173

    
174
    def set_header(self, name, value):
175
        pass
176

    
177
    def reset_headers(self):
178
        self.headers = {}
179

    
180
    def set_param(self, name, value):
181
        self.params = {}
182

    
183
    def reset_params(self):
184
        pass
185

    
186
    def perform_request(self, *args):
187
        return FR()
188

    
189

    
190
class Client(TestCase):
191

    
192
    def assert_dicts_are_equal(self, d1, d2):
193
        for k, v in d1.items():
194
            self.assertTrue(k in d2)
195
            if isinstance(v, dict):
196
                self.assert_dicts_are_equal(v, d2[k])
197
            else:
198
                self.assertEqual(unicode(v), unicode(d2[k]))
199

    
200
    def setUp(self):
201
        from kamaki.clients import Client
202
        from kamaki.clients import ClientError as CE
203
        self.base_url = 'http://example.com'
204
        self.token = 's0m370k3n=='
205
        self.client = Client(self.base_url, self.token, FakeConnection())
206
        self.CE = CE
207

    
208
    def tearDown(self):
209
        FR.text = None
210
        FR.status = None
211
        FR.status_code = 200
212
        FakeConnection.headers = dict()
213
        self.client.token = self.token
214

    
215
    def test___init__(self):
216
        self.assertEqual(self.client.base_url, self.base_url)
217
        self.assertEqual(self.client.token, self.token)
218
        self.assert_dicts_are_equal(self.client.headers, {})
219
        DATE_FORMATS = [
220
            '%a %b %d %H:%M:%S %Y',
221
            '%A, %d-%b-%y %H:%M:%S GMT',
222
            '%a, %d %b %Y %H:%M:%S GMT']
223
        self.assertEqual(self.client.DATE_FORMATS, DATE_FORMATS)
224
        self.assertTrue(isinstance(self.client.http_client, FakeConnection))
225

    
226
    def test__init_thread_limit(self):
227
        exp = 'Nothing set here'
228
        for faulty in (-1, 0.5, 'a string', {}):
229
            self.assertRaises(
230
                AssertionError,
231
                self.client._init_thread_limit,
232
                faulty)
233
            self.assertEqual(exp, getattr(self.client, '_thread_limit', exp))
234
            self.assertEqual(exp, getattr(self.client, '_elapsed_old', exp))
235
            self.assertEqual(exp, getattr(self.client, '_elapsed_new', exp))
236
        self.client._init_thread_limit(42)
237
        self.assertEqual(42, self.client._thread_limit)
238
        self.assertEqual(0.0, self.client._elapsed_old)
239
        self.assertEqual(0.0, self.client._elapsed_new)
240

    
241
    def test__watch_thread_limit(self):
242
        waits = (
243
            dict(args=((0.1, 1), (0.1, 2), (0.2, 1), (0.7, 1), (0.3, 2))),
244
            dict(args=((1.0 - (i / 10.0), (i + 1)) for i in range(7))),
245
            dict(max=1, args=tuple([(randint(1, 10) / 3.0, 1), ] * 10)),
246
            dict(
247
                limit=5,
248
                args=tuple([
249
                    (1.0 + (i / 10.0), (5 - i - 1)) for i in range(4)] + [
250
                    (2.0, 1), (1.9, 2), (2.0, 1), (2.0, 2)])),
251
            dict(args=tuple(
252
                [(1.0 - (i / 10.0), (i + 1)) for i in range(7)] + [
253
                (0.1, 7), (0.2, 6), (0.4, 5), (0.3, 6), (0.2, 7), (0.1, 7)])),)
254
        for wait_dict in waits:
255
            if 'max' in wait_dict:
256
                self.client.MAX_THREADS = wait_dict['max']
257
            else:
258
                self.client.MAX_THREADS = 7
259
            if 'limit' in wait_dict:
260
                self.client._init_thread_limit(wait_dict['limit'])
261
            else:
262
                self.client._init_thread_limit()
263
                self.client._watch_thread_limit(list())
264
                self.assertEqual(1, self.client._thread_limit)
265
            for wait, exp_limit in wait_dict['args']:
266
                self.client._elapsed_new = wait
267
                self.client._watch_thread_limit(list())
268
                self.assertEqual(exp_limit, self.client._thread_limit)
269

    
270
    def test__raise_for_status(self):
271
        r = FR()
272
        for txt, sts_code, sts in (('err msg', 10, None), ('', 42, 'Err St')):
273
            r.text, r.status_code, r.status = txt, sts_code, sts
274
            try:
275
                self.client._raise_for_status(r)
276
            except self.CE as ce:
277
                self.assertEqual('%s' % ce, '%s %s\n' % (sts or '', txt))
278
                self.assertEqual(ce.status, sts_code)
279

    
280
        for msg, sts_code in (('err msg', 32), ('', 42), ('an err', None)):
281
            err = self.CE(msg, sts_code) if sts_code else Exception(msg)
282
            try:
283
                self.client._raise_for_status(err)
284
            except self.CE as ce:
285
                self.assertEqual('%s' % ce, '%s %s\n' % (sts_code or '', msg))
286
                self.assertEqual(ce.status, sts_code or 0)
287

    
288
    @patch('%s.FakeConnection.set_header' % __name__)
289
    def test_set_header(self, SH):
290
        num_of_calls = 0
291
        for name, value, condition in product(
292
                ('n4m3', '', None),
293
                ('v41u3', None, 42),
294
                (True, False, None, 1, '')):
295
            self.client.set_header(name, value, iff=condition)
296
            if value is not None and condition:
297
                self.assertEqual(SH.mock_calls[-1], call(name, value))
298
                num_of_calls += 1
299
            else:
300
                self.assertEqual(num_of_calls, len(SH.mock_calls))
301

    
302
    @patch('%s.FakeConnection.set_param' % __name__)
303
    def test_set_param(self, SP):
304
        num_of_calls = 0
305
        for name, value, condition in product(
306
                ('n4m3', '', None),
307
                ('v41u3', None, 42),
308
                (True, False, None, 1, '')):
309
            self.client.set_param(name, value, iff=condition)
310
            if condition:
311
                self.assertEqual(SP.mock_calls[-1], call(name, value))
312
                num_of_calls += 1
313
            else:
314
                self.assertEqual(num_of_calls, len(SP.mock_calls))
315

    
316
    @patch('%s.FakeConnection.perform_request' % __name__, return_value=FR())
317
    def test_request(self, PR):
318
        for args in product(
319
                ('get', '', dict(method='get')),
320
                ('/some/path', None, ['some', 'path']),
321
                (dict(), dict(h1='v1'), dict(h1='v2', h2='v2')),
322
                (dict(), dict(p1='v1'), dict(p1='v2', p2=None, p3='v3')),
323
                (dict(), dict(data='some data'), dict(
324
                    success=400,
325
                    json=dict(k2='v2', k1='v1')))):
326
            method, path, kwargs = args[0], args[1], args[-1]
327
            args = args[:-1]
328
            if not (isinstance(method, str) and method and isinstance(
329
                    path, str) and path):
330
                self.assertRaises(
331
                    AssertionError,
332
                    self.client.request,
333
                    *args, **kwargs)
334
            else:
335
                atoken = 'a70k3n_%s' % randint(1, 30)
336
                self.client.token = atoken
337
                if 'success' in kwargs:
338
                    self.assertRaises(
339
                        self.CE,
340
                        self.client.request,
341
                        *args, **kwargs)
342
                    FR.status_code = kwargs['success']
343
                else:
344
                    FR.status_code = 200
345
                self.client.request(*args, **kwargs)
346
                data = kwargs.get(
347
                    'data',
348
                    '{"k2": "v2", "k1": "v1"}' if 'json' in kwargs else None)
349
                self.assertEqual(self.client.http_client.url, self.base_url)
350
                self.assertEqual(self.client.http_client.path, path)
351
                self.assertEqual(
352
                    PR.mock_calls[-1],
353
                    call(method, data, *args[2:]))
354
                self.assertEqual(self.client.http_client.headers, dict())
355
                self.assertEqual(self.client.http_client.params, dict())
356

    
357
    @patch('kamaki.clients.Client.request', return_value='lala')
358
    def _test_foo(self, foo, request):
359
        method = getattr(self.client, foo)
360
        r = method('path', k='v')
361
        self.assertEqual(r, 'lala')
362
        request.assert_called_once_with(foo, 'path', k='v')
363

    
364
    def test_delete(self):
365
        self._test_foo('delete')
366

    
367
    def test_get(self):
368
        self._test_foo('get')
369

    
370
    def test_head(self):
371
        self._test_foo('head')
372

    
373
    def test_post(self):
374
        self._test_foo('post')
375

    
376
    def test_put(self):
377
        self._test_foo('put')
378

    
379
    def test_copy(self):
380
        self._test_foo('copy')
381

    
382
    def test_move(self):
383
        self._test_foo('move')
384

    
385

    
386
#  TestCase auxiliary methods
387

    
388
def runTestCase(cls, test_name, args=[], failure_collector=[]):
389
    """
390
    :param cls: (TestCase) a set of Tests
391

392
    :param test_name: (str)
393

394
    :param args: (list) these are prefixed with test_ and used as params when
395
        instantiating cls
396

397
    :param failure_collector: (list) collects info of test failures
398

399
    :returns: (int) total # of run tests
400
    """
401
    suite = TestSuite()
402
    if args:
403
        suite.addTest(cls('_'.join(['test'] + args)))
404
    else:
405
        suite.addTest(makeSuite(cls))
406
    print('* Test * %s *' % test_name)
407
    r = TextTestRunner(verbosity=2).run(suite)
408
    failure_collector += r.failures
409
    return r.testsRun
410

    
411

    
412
def _add_value(foo, value):
413
    def wrap(self):
414
        return foo(self, value)
415
    return wrap
416

    
417

    
418
def get_test_classes(module=__import__(__name__), name=''):
419
    module_stack = [module]
420
    while module_stack:
421
        module = module_stack[-1]
422
        module_stack = module_stack[:-1]
423
        for objname, obj in getmembers(module):
424
            if (objname == name or not name):
425
                if isclass(obj) and objname != 'TestCase' and (
426
                        issubclass(obj, TestCase)):
427
                    yield (obj, objname)
428

    
429

    
430
def main(argv):
431
    found = False
432
    failure_collector = list()
433
    num_of_tests = 0
434
    for cls, name in get_test_classes(name=argv[1] if len(argv) > 1 else ''):
435
        found = True
436
        num_of_tests += runTestCase(cls, name, argv[2:], failure_collector)
437
    if not found:
438
        print('Test "%s" not found' % ' '.join(argv[1:]))
439
    else:
440
        for i, failure in enumerate(failure_collector):
441
            print('Failure %s: ' % (i + 1))
442
            for field in failure:
443
                print('\t%s' % field)
444
        print('\nTotal tests run: %s' % num_of_tests)
445
        print('Total failures: %s' % len(failure_collector))
446

    
447

    
448
if __name__ == '__main__':
449
    from sys import argv
450
    main(argv)