~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Aaron Bentley
  • Date: 2012-01-11 15:44:02 UTC
  • Revision ID: aaron@canonical.com-20120111154402-qi5nk23wwd7czvrj
Include next_memo, previous_memo in get_messages response.

Show diffs side-by-side

added added

removed removed

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