~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(httplib.BAD_REQUEST)
231
 
                self.wfile.write('Unsupported order')
232
 
                return
233
 
 
234
 
    def log_message(self, format, *args):
235
 
        """Override log_message to use standard Python logging."""
236
 
        message = "%s - - [%s] %s\n" % (
237
 
            self.address_string(), self.log_date_time_string(), format % args)
238
 
        self.logger.info(message)
239
 
 
240
 
 
241
 
class TestPutMessage(TestCase):
242
 
 
243
 
    def test_put_message(self):
244
 
        client = GrackleClient('localhost', 8436)
245
 
        with ForkedFakeService.from_client(client):
246
 
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
247
 
            with ExpectedException(Exception, 'wtf'):
248
 
                client.put_message('arch1', 'asdf',
249
 
                    StringIO('This is not a message'))
250
 
 
251
 
 
252
 
class TestGetMessages(TestCase):
253
 
 
254
 
    def assertIDOrder(self, ids, messages):
255
 
        self.assertEqual(ids, [m['message_id'] for m in messages])
256
 
 
257
 
    def assertMessageIDs(self, ids, messages):
258
 
        self.assertIDOrder(
259
 
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
260
 
 
261
 
    def test_get_messages(self):
262
 
        client = GrackleClient('localhost', 8435)
263
 
        with ForkedFakeService.from_client(client,
264
 
            {'baz':
265
 
            [{'message_id': 'foo'},
266
 
             {'message_id': 'bar'}]}):
267
 
            response = client.get_messages('baz')
268
 
        self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
269
 
            response['messages']))
270
 
        self.assertIs(None, response['next_memo'])
271
 
        self.assertIs(None, response['previous_memo'])
272
 
 
273
 
    def test_get_messages_by_id(self):
274
 
        client = GrackleClient('localhost', 8437)
275
 
        with ForkedFakeService.from_client(client,
276
 
            {'baz':
277
 
            [{'message_id': 'foo'},
278
 
             {'message_id': 'bar'}]}):
279
 
            response = client.get_messages('baz', message_ids=['foo'])
280
 
        message, = response['messages']
281
 
        self.assertEqual('foo', message['message_id'])
282
 
 
283
 
    def test_get_messages_batching(self):
284
 
        client = GrackleClient('localhost', 8438)
285
 
        with ForkedFakeService.from_client(client,
286
 
            {'baz':
287
 
            [{'message_id': 'foo'},
288
 
             {'message_id': 'bar'}]}):
289
 
            response = client.get_messages('baz', limit=1)
290
 
            self.assertEqual(1, len(response['messages']))
291
 
            messages = response['messages']
292
 
            response = client.get_messages(
293
 
                'baz', limit=1, memo=response['next_memo'])
294
 
            self.assertEqual(1, len(response['messages']))
295
 
            messages.extend(response['messages'])
296
 
            self.assertMessageIDs(['foo', 'bar'], messages)
297
 
 
298
 
    def get_messages_member_order_test(self, key):
299
 
        client = GrackleClient('localhost', 8439)
300
 
        with ForkedFakeService.from_client(client,
301
 
                {'baz': [{'message_id': 'foo', key: '2011-03-25'},
302
 
                 {'message_id': 'bar', key: '2011-03-24'}]}):
303
 
            response = client.get_messages('baz')
304
 
            self.assertIDOrder(['foo', 'bar'], response['messages'])
305
 
            response = client.get_messages('baz', order=key)
306
 
            self.assertIDOrder(['bar', 'foo'], response['messages'])
307
 
 
308
 
    def test_get_messages_date_order(self):
309
 
        self.get_messages_member_order_test('date')
310
 
 
311
 
    def test_get_messages_author_order(self):
312
 
        self.get_messages_member_order_test('author')
313
 
 
314
 
    def test_get_messages_subject_order(self):
315
 
        self.get_messages_member_order_test('subject')
316
 
 
317
 
    def test_get_messages_thread_subject_order(self):
318
 
        client = GrackleClient('localhost', 8439)
319
 
        with ForkedFakeService.from_client(client, {'baz': [
320
 
            {'message_id': 'bar', 'subject': 'y'},
321
 
            {'message_id': 'qux', 'subject': 'z'},
322
 
            {'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
323
 
            ]}):
324
 
            response = client.get_messages('baz')
325
 
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
326
 
            response = client.get_messages('baz', order='subject')
327
 
            self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
328
 
            response = client.get_messages('baz', order='thread_subject')
329
 
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
330
 
 
331
 
    def test_get_messages_thread_oldest_order(self):
332
 
        client = GrackleClient('localhost', 8439)
333
 
        with ForkedFakeService.from_client(client, {'baz': [
334
 
            {'message_id': 'bar', 'date': 'x'},
335
 
            {'message_id': 'qux', 'date': 'z'},
336
 
            {'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
337
 
            ]}):
338
 
            response = client.get_messages('baz')
339
 
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
340
 
            response = client.get_messages('baz', order='date')
341
 
            self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
342
 
            response = client.get_messages('baz', order='thread_oldest')
343
 
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
344
 
 
345
 
    def test_get_messages_thread_newest_order(self):
346
 
        client = GrackleClient('localhost', 8439)
347
 
        with ForkedFakeService.from_client(client, {'baz': [
348
 
            {'message_id': 'bar', 'date': 'x'},
349
 
            {'message_id': 'qux', 'date': 'w'},
350
 
            {'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
351
 
            {'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
352
 
            ]}):
353
 
            response = client.get_messages('baz', order='date')
354
 
            self.assertIDOrder(
355
 
                ['qux', 'bar', 'foo', 'baz'], response['messages'])
356
 
            response = client.get_messages('baz', order='thread_newest')
357
 
            self.assertIDOrder(
358
 
                ['bar', 'foo', 'qux', 'baz'], response['messages'])
359
 
 
360
 
    def test_get_messages_unsupported_order(self):
361
 
        client = GrackleClient('localhost', 8439)
362
 
        with ForkedFakeService.from_client(client,
363
 
                {'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
364
 
                 {'message_id': 'bar', 'date': '2011-03-24'}]}):
365
 
            with ExpectedException(UnsupportedOrder, ''):
366
 
                client.get_messages('baz', order='nonsense')
367
 
 
368
 
    def test_get_messages_headers_no_headers(self):
369
 
        client = GrackleClient('localhost', 8440)
370
 
        with ForkedFakeService.from_client(client,
371
 
            {'baz': [
372
 
                {'message_id': 'foo'}
373
 
            ]}):
374
 
            response = client.get_messages('baz', headers=[
375
 
                'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
376
 
        first_message = response['messages'][0]
377
 
        self.assertEqual('foo', first_message['message_id'])
378
 
        self.assertEqual({}, first_message['headers'])
379
 
 
380
 
    def test_get_messages_headers_exclude_headers(self):
381
 
        client = GrackleClient('localhost', 8441)
382
 
        with ForkedFakeService.from_client(client,
383
 
            {'baz': [
384
 
                {'message_id': 'foo', 'headers': {'From': 'me'}}
385
 
            ]}):
386
 
            response = client.get_messages('baz', headers=[
387
 
                'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
388
 
        first_message = response['messages'][0]
389
 
        self.assertEqual('foo', first_message['message_id'])
390
 
        self.assertEqual({}, first_message['headers'])
391
 
 
392
 
    def test_get_messages_headers_include_headers(self):
393
 
        client = GrackleClient('localhost', 8442)
394
 
        with ForkedFakeService.from_client(client,
395
 
            {'baz': [
396
 
                {'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
397
 
            ]}):
398
 
            response = client.get_messages('baz', headers=[
399
 
                'From', 'To'])
400
 
        first_message = response['messages'][0]
401
 
        self.assertEqual('foo', first_message['message_id'])
402
 
        self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
403
 
 
404
 
    def test_get_messages_max_body_length(self):
405
 
        client = GrackleClient('localhost', 8443)
406
 
        with ForkedFakeService.from_client(client,
407
 
            {'baz': [
408
 
                {'message_id': 'foo', 'body': u'abcdefghi'}
409
 
            ]}):
410
 
            response = client.get_messages('baz', max_body_length=3)
411
 
        first_message = response['messages'][0]
412
 
        self.assertEqual('abc', first_message['body'])
413
 
 
414
 
    def test_include_hidden(self):
415
 
        client = GrackleClient('localhost', 8444)
416
 
        with ForkedFakeService.from_client(client,
417
 
            {'baz': [
418
 
                {'message_id': 'foo', 'hidden': True},
419
 
                {'message_id': 'bar', 'hidden': False}
420
 
            ]}):
421
 
            response = client.get_messages('baz', include_hidden=True)
422
 
            self.assertMessageIDs(['bar', 'foo'], response['messages'])
423
 
            response = client.get_messages('baz', include_hidden=False)
424
 
            self.assertMessageIDs(['bar'], response['messages'])
425
 
 
426
 
    def test_display_type_unknown_value(self):
427
 
        client = GrackleClient('localhost', 8445)
428
 
        with ForkedFakeService.from_client(client,
429
 
            {'baz': [
430
 
                {'message_id': 'foo', 'body': u'abcdefghi'}
431
 
            ]}):
432
 
            with ExpectedException(UnsupportedDisplayType, ''):
433
 
                client.get_messages('baz', display_type='unknown')
434
 
 
435
 
    def test_display_type_headers_only(self):
436
 
        client = GrackleClient('localhost', 8445)
437
 
        with ForkedFakeService.from_client(client,
438
 
            {'baz': [
439
 
                {'message_id': 'foo',
440
 
                 'headers': {'From': 'me', 'To': 'you'},
441
 
                 'body': 'abcdefghi'}
442
 
            ]}):
443
 
            response = client.get_messages('baz', display_type='headers-only')
444
 
        first_message = response['messages'][0]
445
 
        self.assertEqual('foo', first_message['message_id'])
446
 
        self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
447
 
        self.assertNotIn('body', first_message)