~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:37:22 UTC
  • mto: This revision was merged to the branch mainline in revision 37.
  • Revision ID: curtis.hovey@canonical.com-20120130203722-gjzvehl3bdir11nl
Moved the display_type arg.

Show diffs side-by-side

added added

removed removed

Lines of Context:
3
3
    BaseHTTPRequestHandler,
4
4
    )
5
5
import httplib
 
6
import logging
6
7
import os
7
8
from signal import SIGKILL
 
9
import simplejson
8
10
from StringIO import StringIO
 
11
import sys
9
12
from unittest import TestCase
 
13
from urlparse import urlparse
 
14
from urlparse import parse_qs
10
15
 
11
16
from testtools import ExpectedException
12
17
 
13
18
from grackle.client import (
14
19
    GrackleClient,
 
20
    UnsupportedDisplayType,
 
21
    UnsupportedOrder,
15
22
    )
16
23
 
17
24
 
18
 
class Forked:
19
 
 
20
 
    def __init__(self, func_or_method, *args):
21
 
        self.func_or_method = func_or_method
 
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
        """
22
136
        self.pid = None
23
 
        self.args = args
 
137
        self.port = port
 
138
        if messages is None:
 
139
            self.messages = {}
 
140
        else:
 
141
            self.messages = messages
 
142
        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
 
 
156
    def is_ready(self):
 
157
        """Tell the parent process that the server is ready for writes."""
 
158
        os.write(self.write_end, 'asdf')
24
159
 
25
160
    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."""
26
165
        pid = os.fork()
27
 
        if pid != 0:
28
 
            self.pid = pid
29
 
            return
30
 
        self.func_or_method(*self.args)
 
166
        if pid == 0:
 
167
            self.start_server()
 
168
        self.pid = pid
 
169
        os.read(self.read_end, 1)
 
170
        return
31
171
 
 
172
    def start_server(self):
 
173
        """Start the HTTP server."""
 
174
        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', {})
 
179
        self.is_ready()
 
180
        if self.write_logs:
 
181
            logging.basicConfig(
 
182
                stream=sys.stderr, level=logging.INFO)
 
183
        service.serve_forever()
32
184
 
33
185
    def __exit__(self, exc_type, exc_val, traceback):
34
186
        os.kill(self.pid, SIGKILL)
35
187
 
36
188
 
 
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
 
37
197
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
38
 
 
39
 
    def do_PUT(self):
 
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
 
 
205
    def do_POST(self):
 
206
        """Create a message on POST."""
40
207
        message = self.rfile.read(int(self.headers['content-length']))
41
208
        if message == 'This is a message':
42
209
            self.send_response(httplib.CREATED)
45
212
        else:
46
213
            self.send_error(httplib.BAD_REQUEST)
47
214
 
48
 
 
49
 
def run_service(port):
50
 
    service = HTTPServer(('', port), FakeGrackleRequestHandler)
51
 
    service.serve_forever()
52
 
 
 
215
    def do_GET(self):
 
216
        """Retrieve a list of messages on GET."""
 
217
        scheme, netloc, path, params, query_string, fragments = (
 
218
            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)
53
237
 
54
238
 
55
239
class TestPutMessage(TestCase):
56
240
 
57
241
    def test_put_message(self):
58
 
        client = GrackleClient('localhost', 8435)
59
 
        with Forked(run_service, client.port):
 
242
        client = GrackleClient('localhost', 8436)
 
243
        with ForkedFakeService.from_client(client):
60
244
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
61
245
            with ExpectedException(Exception, 'wtf'):
62
246
                client.put_message('arch1', 'asdf',
63
247
                    StringIO('This is not a message'))
 
248
 
 
249
 
 
250
class TestGetMessages(TestCase):
 
251
 
 
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
    def test_get_messages(self):
 
260
        client = GrackleClient('localhost', 8435)
 
261
        with ForkedFakeService.from_client(client,
 
262
            {'baz':
 
263
            [{'message_id': 'foo'},
 
264
             {'message_id': 'bar'}]}):
 
265
            response = client.get_messages('baz')
 
266
        self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
 
267
            response['messages']))
 
268
        self.assertIs(None, response['next_memo'])
 
269
        self.assertIs(None, response['previous_memo'])
 
270
 
 
271
    def test_get_messages_by_id(self):
 
272
        client = GrackleClient('localhost', 8437)
 
273
        with ForkedFakeService.from_client(client,
 
274
            {'baz':
 
275
            [{'message_id': 'foo'},
 
276
             {'message_id': 'bar'}]}):
 
277
            response = client.get_messages('baz', message_ids=['foo'])
 
278
        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')