~unity-2d-team/unity-2d/Shell-MultiMonitor

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Curtis Hovey
  • Date: 2012-01-31 03:22:02 UTC
  • mto: (6.1.31 trunk)
  • mto: This revision was merged to the branch mainline in revision 45.
  • Revision ID: curtis.hovey@canonical.com-20120131032202-b4r9msw4107zvkig
Reanme make_json_message => make_message.

Show diffs side-by-side

added added

removed removed

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