~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: William Grant
  • Date: 2012-01-25 06:19:56 UTC
  • Revision ID: william.grant@canonical.com-20120125061956-4tltjt6a4xf5yufj
Fix test.

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