~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 13:37:23 UTC
  • Revision ID: aaron@canonical.com-20120111133723-cv7qfdgpd4zpk561
Cleanup.

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