~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Aaron Bentley
  • Date: 2012-01-10 10:46:26 UTC
  • Revision ID: aaron@canonical.com-20120110104626-39ehw9nhnzdzggtw
Add README and LICENSE

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from BaseHTTPServer import (
2
 
    HTTPServer,
3
 
    BaseHTTPRequestHandler,
4
 
    )
5
 
import httplib
6
 
import logging
7
 
import os
8
 
from signal import SIGKILL
9
 
import simplejson
10
 
from StringIO import StringIO
11
 
import sys
12
 
from unittest import TestCase
13
 
from urlparse import urlparse
14
 
from urlparse import parse_qs
15
 
 
16
 
from testtools import ExpectedException
17
 
 
18
 
from grackle.client import (
19
 
    GrackleClient,
20
 
    UnsupportedDisplayType,
21
 
    UnsupportedOrder,
22
 
    )
23
 
 
24
 
 
25
 
def threaded_messages(messages):
26
 
    threads = {}
27
 
    count = 0
28
 
    pending = []
29
 
    for message in messages:
30
 
        if message.get('in_reply_to') is None:
31
 
            threads[message['message_id']] = [message]
32
 
            count += 1
33
 
        else:
34
 
            pending.append(message)
35
 
    for message in pending:
36
 
        threads[message['in_reply_to']].append(message)
37
 
    return threads.values()
38
 
 
39
 
 
40
 
class GrackleStore:
41
 
    """A memory-backed message store."""
42
 
 
43
 
    def __init__(self, messages):
44
 
        """Constructor."""
45
 
        self.messages = messages
46
 
 
47
 
    def get_messages(self, archive_id, query_string):
48
 
        """Return matching messages.
49
 
 
50
 
        :param archive_id: The archive to retrieve from.
51
 
        :param query_string: Contains 'parameters', which is a JSON-format
52
 
            string describing parameters.
53
 
        """
54
 
        query = parse_qs(query_string)
55
 
        parameters = simplejson.loads(query['parameters'][0])
56
 
        order = parameters.get('order')
57
 
        messages = self.messages[archive_id]
58
 
        if order is not None:
59
 
            if order not in SUPPORTED_ORDERS:
60
 
                raise UnsupportedOrder
61
 
            elif order.startswith('thread_'):
62
 
                threaded = threaded_messages(messages)
63
 
                messages = []
64
 
                if order == 'thread_subject':
65
 
                    threaded.sort(key=lambda t: t[0]['subject'])
66
 
                if order == 'thread_oldest':
67
 
                    threaded.sort(key=lambda t: min(m['date'] for m in t))
68
 
                if order == 'thread_newest':
69
 
                    threaded.sort(key=lambda t: max(m['date'] for m in t))
70
 
                for thread in threaded:
71
 
                    messages.extend(thread)
72
 
            else:
73
 
                messages.sort(key=lambda m: m[order])
74
 
        display_type = parameters.get('display_type', 'all')
75
 
        if display_type not in SUPPORTED_DISPLAY_TYPES:
76
 
            raise UnsupportedDisplayType
77
 
        new_messages = []
78
 
        for message in messages:
79
 
            if (not parameters['include_hidden']
80
 
                and message.get('hidden', False)):
81
 
                continue
82
 
 
83
 
            if ('message_ids' in parameters
84
 
                and message['message_id'] not in parameters['message_ids']):
85
 
                continue
86
 
            message = dict(message)
87
 
            if 'headers' in parameters:
88
 
                headers = dict(
89
 
                    (k, v) for k, v in message['headers'].iteritems()
90
 
                    if k in parameters['headers'])
91
 
                message['headers'] = headers
92
 
            max_body = parameters.get('max_body_length')
93
 
            if display_type == 'headers-only':
94
 
                del message['body']
95
 
            elif max_body is not None:
96
 
                message['body'] = message['body'][:max_body]
97
 
            new_messages.append(message)
98
 
        messages = new_messages
99
 
        limit = parameters.get('limit', 100)
100
 
        memo = parameters.get('memo')
101
 
        message_id_indices = dict(
102
 
            (m['message_id'], idx) for idx, m in enumerate(messages))
103
 
        if memo is None:
104
 
            start = 0
105
 
        else:
106
 
            start = message_id_indices[memo.encode('rot13')]
107
 
        if start > 0:
108
 
            previous_memo = messages[start - 1]['message_id'].encode('rot13')
109
 
        else:
110
 
            previous_memo = None
111
 
        end = min(start + limit, len(messages))
112
 
        if end < len(messages):
113
 
            next_memo = messages[end]['message_id'].encode('rot13')
114
 
        else:
115
 
            next_memo = None
116
 
        messages = messages[start:end]
117
 
 
118
 
        response = {
119
 
            'messages': messages,
120
 
            'next_memo': next_memo,
121
 
            'previous_memo': previous_memo
122
 
            }
123
 
        return response
124
 
 
125
 
 
126
 
class ForkedFakeService:
127
 
    """A Grackle service fake, as a ContextManager."""
128
 
 
129
 
    def __init__(self, port, messages=None, write_logs=False):
130
 
        """Constructor.
131
 
 
132
 
        :param port: The tcp port to use.
133
 
        :param messages: A dict of lists of dicts representing messages.  The
134
 
            outer dict represents the archive, the list represents the list of
135
 
            messages for that archive.
136
 
        :param write_logs: If true, log messages will be written to stdout.
137
 
        """
138
 
        self.pid = None
139
 
        self.port = port
140
 
        if messages is None:
141
 
            self.messages = {}
142
 
        else:
143
 
            self.messages = messages
144
 
        self.read_end, self.write_end = os.pipe()
145
 
        self.write_logs = write_logs
146
 
 
147
 
    @staticmethod
148
 
    def from_client(client, messages=None):
149
 
        """Instantiate a ForkedFakeService from the client.
150
 
 
151
 
        :param port: The client to provide service for.
152
 
        :param messages: A dict of lists of dicts representing messages.  The
153
 
            outer dict represents the archive, the list represents the list of
154
 
            messages for that archive.
155
 
        """
156
 
        return ForkedFakeService(client.port, messages)
157
 
 
158
 
    def is_ready(self):
159
 
        """Tell the parent process that the server is ready for writes."""
160
 
        os.write(self.write_end, 'asdf')
161
 
 
162
 
    def __enter__(self):
163
 
        """Run the service.
164
 
 
165
 
        Fork and start a server in the child.  Return when the server is ready
166
 
        for use."""
167
 
        pid = os.fork()
168
 
        if pid == 0:
169
 
            self.start_server()
170
 
        self.pid = pid
171
 
        os.read(self.read_end, 1)
172
 
        return
173
 
 
174
 
    def start_server(self):
175
 
        """Start the HTTP server."""
176
 
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
177
 
        service.store = GrackleStore(self.messages)
178
 
        for archive_id, messages in service.store.messages.iteritems():
179
 
            for message in messages:
180
 
                message.setdefault('headers', {})
181
 
        self.is_ready()
182
 
        if self.write_logs:
183
 
            logging.basicConfig(
184
 
                stream=sys.stderr, level=logging.INFO)
185
 
        service.serve_forever()
186
 
 
187
 
    def __exit__(self, exc_type, exc_val, traceback):
188
 
        os.kill(self.pid, SIGKILL)
189
 
 
190
 
 
191
 
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
192
 
 
193
 
 
194
 
SUPPORTED_ORDERS = set(
195
 
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
196
 
     'thread_subject'])
197
 
 
198
 
 
199
 
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
200
 
    """A request handler that forwards to server.store."""
201
 
 
202
 
    def __init__(self, *args, **kwargs):
203
 
        """Constructor.  Sets up logging."""
204
 
        self.logger = logging.getLogger('http')
205
 
        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
206
 
 
207
 
    def do_POST(self):
208
 
        """Create a message on POST."""
209
 
        message = self.rfile.read(int(self.headers['content-length']))
210
 
        if message == 'This is a message':
211
 
            self.send_response(httplib.CREATED)
212
 
            self.end_headers()
213
 
            self.wfile.close()
214
 
        else:
215
 
            self.send_error(httplib.BAD_REQUEST)
216
 
 
217
 
    def do_GET(self):
218
 
        """Retrieve a list of messages on GET."""
219
 
        scheme, netloc, path, params, query_string, fragments = (
220
 
            urlparse(self.path))
221
 
        parts = path.split('/')
222
 
        if parts[1] == 'archive':
223
 
            try:
224
 
                response = self.server.store.get_messages(
225
 
                    parts[2], query_string)
226
 
                self.send_response(httplib.OK)
227
 
                self.end_headers()
228
 
                self.wfile.write(simplejson.dumps(response))
229
 
            except UnsupportedOrder:
230
 
                self.send_response(
231
 
                    httplib.BAD_REQUEST, UnsupportedOrder.__doc__)
232
 
                return
233
 
            except UnsupportedDisplayType:
234
 
                self.send_response(
235
 
                    httplib.BAD_REQUEST, UnsupportedDisplayType.__doc__)
236
 
                return
237
 
 
238
 
    def log_message(self, format, *args):
239
 
        """Override log_message to use standard Python logging."""
240
 
        message = "%s - - [%s] %s\n" % (
241
 
            self.address_string(), self.log_date_time_string(), format % args)
242
 
        self.logger.info(message)
243
 
 
244
 
 
245
 
class TestPutMessage(TestCase):
246
 
 
247
 
    def test_put_message(self):
248
 
        client = GrackleClient('localhost', 8436)
249
 
        with ForkedFakeService.from_client(client):
250
 
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
251
 
            with ExpectedException(Exception, 'wtf'):
252
 
                client.put_message('arch1', 'asdf',
253
 
                    StringIO('This is not a message'))
254
 
 
255
 
 
256
 
class TestGetMessages(TestCase):
257
 
 
258
 
    def assertIDOrder(self, ids, messages):
259
 
        self.assertEqual(ids, [m['message_id'] for m in messages])
260
 
 
261
 
    def assertMessageIDs(self, ids, messages):
262
 
        self.assertIDOrder(
263
 
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
264
 
 
265
 
    def test_get_messages(self):
266
 
        client = GrackleClient('localhost', 8435)
267
 
        with ForkedFakeService.from_client(client,
268
 
            {'baz':
269
 
            [{'message_id': 'foo'},
270
 
             {'message_id': 'bar'}]}):
271
 
            response = client.get_messages('baz')
272
 
        self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
273
 
            response['messages']))
274
 
        self.assertIs(None, response['next_memo'])
275
 
        self.assertIs(None, response['previous_memo'])
276
 
 
277
 
    def test_get_messages_by_id(self):
278
 
        client = GrackleClient('localhost', 8437)
279
 
        with ForkedFakeService.from_client(client,
280
 
            {'baz':
281
 
            [{'message_id': 'foo'},
282
 
             {'message_id': 'bar'}]}):
283
 
            response = client.get_messages('baz', message_ids=['foo'])
284
 
        message, = response['messages']
285
 
        self.assertEqual('foo', message['message_id'])
286
 
 
287
 
    def test_get_messages_batching(self):
288
 
        client = GrackleClient('localhost', 8438)
289
 
        with ForkedFakeService.from_client(client,
290
 
            {'baz':
291
 
            [{'message_id': 'foo'},
292
 
             {'message_id': 'bar'}]}):
293
 
            response = client.get_messages('baz', limit=1)
294
 
            self.assertEqual(1, len(response['messages']))
295
 
            messages = response['messages']
296
 
            response = client.get_messages(
297
 
                'baz', limit=1, memo=response['next_memo'])
298
 
            self.assertEqual(1, len(response['messages']))
299
 
            messages.extend(response['messages'])
300
 
            self.assertMessageIDs(['foo', 'bar'], messages)
301
 
 
302
 
    def get_messages_member_order_test(self, key):
303
 
        client = GrackleClient('localhost', 8439)
304
 
        with ForkedFakeService.from_client(client,
305
 
                {'baz': [{'message_id': 'foo', key: '2011-03-25'},
306
 
                 {'message_id': 'bar', key: '2011-03-24'}]}):
307
 
            response = client.get_messages('baz')
308
 
            self.assertIDOrder(['foo', 'bar'], response['messages'])
309
 
            response = client.get_messages('baz', order=key)
310
 
            self.assertIDOrder(['bar', 'foo'], response['messages'])
311
 
 
312
 
    def test_get_messages_date_order(self):
313
 
        self.get_messages_member_order_test('date')
314
 
 
315
 
    def test_get_messages_author_order(self):
316
 
        self.get_messages_member_order_test('author')
317
 
 
318
 
    def test_get_messages_subject_order(self):
319
 
        self.get_messages_member_order_test('subject')
320
 
 
321
 
    def test_get_messages_thread_subject_order(self):
322
 
        client = GrackleClient('localhost', 8439)
323
 
        with ForkedFakeService.from_client(client, {'baz': [
324
 
            {'message_id': 'bar', 'subject': 'y'},
325
 
            {'message_id': 'qux', 'subject': 'z'},
326
 
            {'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
327
 
            ]}):
328
 
            response = client.get_messages('baz')
329
 
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
330
 
            response = client.get_messages('baz', order='subject')
331
 
            self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
332
 
            response = client.get_messages('baz', order='thread_subject')
333
 
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
334
 
 
335
 
    def test_get_messages_thread_oldest_order(self):
336
 
        client = GrackleClient('localhost', 8439)
337
 
        with ForkedFakeService.from_client(client, {'baz': [
338
 
            {'message_id': 'bar', 'date': 'x'},
339
 
            {'message_id': 'qux', 'date': 'z'},
340
 
            {'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
341
 
            ]}):
342
 
            response = client.get_messages('baz')
343
 
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
344
 
            response = client.get_messages('baz', order='date')
345
 
            self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
346
 
            response = client.get_messages('baz', order='thread_oldest')
347
 
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
348
 
 
349
 
    def test_get_messages_thread_newest_order(self):
350
 
        client = GrackleClient('localhost', 8439)
351
 
        with ForkedFakeService.from_client(client, {'baz': [
352
 
            {'message_id': 'bar', 'date': 'x'},
353
 
            {'message_id': 'qux', 'date': 'w'},
354
 
            {'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
355
 
            {'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
356
 
            ]}):
357
 
            response = client.get_messages('baz', order='date')
358
 
            self.assertIDOrder(
359
 
                ['qux', 'bar', 'foo', 'baz'], response['messages'])
360
 
            response = client.get_messages('baz', order='thread_newest')
361
 
            self.assertIDOrder(
362
 
                ['bar', 'foo', 'qux', 'baz'], response['messages'])
363
 
 
364
 
    def test_get_messages_unsupported_order(self):
365
 
        client = GrackleClient('localhost', 8439)
366
 
        with ForkedFakeService.from_client(client,
367
 
                {'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
368
 
                 {'message_id': 'bar', 'date': '2011-03-24'}]}):
369
 
            with ExpectedException(UnsupportedOrder, ''):
370
 
                client.get_messages('baz', order='nonsense')
371
 
 
372
 
    def test_get_messages_headers_no_headers(self):
373
 
        client = GrackleClient('localhost', 8440)
374
 
        with ForkedFakeService.from_client(client,
375
 
            {'baz': [
376
 
                {'message_id': 'foo'}
377
 
            ]}):
378
 
            response = client.get_messages('baz', headers=[
379
 
                'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
380
 
        first_message = response['messages'][0]
381
 
        self.assertEqual('foo', first_message['message_id'])
382
 
        self.assertEqual({}, first_message['headers'])
383
 
 
384
 
    def test_get_messages_headers_exclude_headers(self):
385
 
        client = GrackleClient('localhost', 8441)
386
 
        with ForkedFakeService.from_client(client,
387
 
            {'baz': [
388
 
                {'message_id': 'foo', 'headers': {'From': 'me'}}
389
 
            ]}):
390
 
            response = client.get_messages('baz', headers=[
391
 
                'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
392
 
        first_message = response['messages'][0]
393
 
        self.assertEqual('foo', first_message['message_id'])
394
 
        self.assertEqual({}, first_message['headers'])
395
 
 
396
 
    def test_get_messages_headers_include_headers(self):
397
 
        client = GrackleClient('localhost', 8442)
398
 
        with ForkedFakeService.from_client(client,
399
 
            {'baz': [
400
 
                {'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
401
 
            ]}):
402
 
            response = client.get_messages('baz', headers=[
403
 
                'From', 'To'])
404
 
        first_message = response['messages'][0]
405
 
        self.assertEqual('foo', first_message['message_id'])
406
 
        self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
407
 
 
408
 
    def test_get_messages_max_body_length(self):
409
 
        client = GrackleClient('localhost', 8443)
410
 
        with ForkedFakeService.from_client(client,
411
 
            {'baz': [
412
 
                {'message_id': 'foo', 'body': u'abcdefghi'}
413
 
            ]}):
414
 
            response = client.get_messages('baz', max_body_length=3)
415
 
        first_message = response['messages'][0]
416
 
        self.assertEqual('abc', first_message['body'])
417
 
 
418
 
    def test_include_hidden(self):
419
 
        client = GrackleClient('localhost', 8444)
420
 
        with ForkedFakeService.from_client(client,
421
 
            {'baz': [
422
 
                {'message_id': 'foo', 'hidden': True},
423
 
                {'message_id': 'bar', 'hidden': False}
424
 
            ]}):
425
 
            response = client.get_messages('baz', include_hidden=True)
426
 
            self.assertMessageIDs(['bar', 'foo'], response['messages'])
427
 
            response = client.get_messages('baz', include_hidden=False)
428
 
            self.assertMessageIDs(['bar'], response['messages'])
429
 
 
430
 
    def test_display_type_unknown_value(self):
431
 
        client = GrackleClient('localhost', 8445)
432
 
        with ForkedFakeService.from_client(client,
433
 
            {'baz': [
434
 
                {'message_id': 'foo', 'body': u'abcdefghi'}
435
 
            ]}):
436
 
            with ExpectedException(UnsupportedDisplayType, ''):
437
 
                client.get_messages('baz', display_type='unknown')
438
 
 
439
 
    def test_display_type_headers_only(self):
440
 
        client = GrackleClient('localhost', 8445)
441
 
        with ForkedFakeService.from_client(client,
442
 
            {'baz': [
443
 
                {'message_id': 'foo',
444
 
                 'headers': {'From': 'me', 'To': 'you'},
445
 
                 'body': 'abcdefghi'}
446
 
            ]}):
447
 
            response = client.get_messages('baz', display_type='headers-only')
448
 
        first_message = response['messages'][0]
449
 
        self.assertEqual('foo', first_message['message_id'])
450
 
        self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
451
 
        self.assertNotIn('body', first_message)