~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Aaron Bentley
  • Date: 2012-01-10 14:41:33 UTC
  • Revision ID: aaron@canonical.com-20120110144133-p9dk4oktqnp3lnlw
Fix URLs etc.

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