~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 11:36:08 UTC
  • Revision ID: aaron@canonical.com-20120111113608-fgygml5scw5orr0j
Use pipe to ensure we only use HTTP once it's running.

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
 
import simplejson
10
8
from StringIO import StringIO
11
 
import sys
12
9
from unittest import TestCase
13
 
from urlparse import urlparse
14
 
from urlparse import parse_qs
15
10
 
16
11
from testtools import ExpectedException
17
12
 
18
13
from grackle.client import (
19
14
    GrackleClient,
20
 
    UnsupportedOrder,
21
15
    )
22
16
 
23
17
 
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
18
class ForkedFake:
123
 
    """A Grackle service fake, as a ContextManager."""
124
19
 
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
 
        """
 
20
    def __init__(self, port, messages=None):
133
21
        self.pid = None
134
22
        self.port = port
135
 
        if messages is None:
136
 
            self.messages = {}
137
 
        else:
138
 
            self.messages = messages
 
23
        self.messages = messages
139
24
        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
25
 
153
26
    def is_ready(self):
154
 
        """Tell the parent process that the server is ready for writes."""
155
27
        os.write(self.write_end, 'asdf')
156
28
 
157
29
    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."""
162
30
        pid = os.fork()
163
31
        if pid == 0:
164
32
            self.start_server()
167
35
        return
168
36
 
169
37
    def start_server(self):
170
 
        """Start the HTTP server."""
171
38
        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', {})
 
39
        service.messages = self.messages
176
40
        self.is_ready()
177
 
        if self.write_logs:
178
 
            logging.basicConfig(
179
 
                stream=sys.stderr, level=logging.INFO)
180
41
        service.serve_forever()
181
42
 
182
43
    def __exit__(self, exc_type, exc_val, traceback):
183
44
        os.kill(self.pid, SIGKILL)
184
45
 
185
46
 
186
 
SUPPORTED_ORDERS = set(
187
 
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
188
 
     'thread_subject'])
189
 
 
190
 
 
191
47
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
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
48
 
199
49
    def do_POST(self):
200
 
        """Create a message on POST."""
201
50
        message = self.rfile.read(int(self.headers['content-length']))
202
51
        if message == 'This is a message':
203
52
            self.send_response(httplib.CREATED)
206
55
        else:
207
56
            self.send_error(httplib.BAD_REQUEST)
208
57
 
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
58
 
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)
 
59
def fake_grackle_service(client, messages=None):
 
60
    if messages is None:
 
61
        messages = {}
 
62
    return ForkedFake(client.port, messages)
231
63
 
232
64
 
233
65
class TestPutMessage(TestCase):
234
66
 
235
67
    def test_put_message(self):
236
68
        client = GrackleClient('localhost', 8436)
237
 
        with ForkedFake.from_client(client):
 
69
        with fake_grackle_service(client):
238
70
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
239
71
            with ExpectedException(Exception, 'wtf'):
240
72
                client.put_message('arch1', 'asdf',
243
75
 
244
76
class TestGetMessages(TestCase):
245
77
 
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
78
    def test_get_messages(self):
254
79
        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
 
 
 
80
        with fake_grackle_service(client,
 
81
            {'baz':
 
82
            [{'message-id': 'foo'},
 
83
             {'message-id': 'bar'}]}):
 
84
            response = client.get_messages('baz')
 
85
        self.assertEqual(['bar', 'foo'], sorted(response.keys()))