~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Curtis Hovey
  • Date: 2012-01-30 20:57:08 UTC
  • mto: This revision was merged to the branch mainline in revision 37.
  • Revision ID: curtis.hovey@canonical.com-20120130205708-1sxt295z6nvbsmfg
Added display_type == 'headers-only' support.

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)