~didrocks/unity/altf10

4 by Aaron Bentley
Initial test.
1
from BaseHTTPServer import (
2
    HTTPServer,
3
    BaseHTTPRequestHandler,
4
    )
6 by Aaron Bentley
Use constants.
5
import httplib
32 by Aaron Bentley
Switch test HTTP server to standard Python logging.
6
import logging
4 by Aaron Bentley
Initial test.
7
import os
5 by Aaron Bentley
Actual fake service working.
8
from signal import SIGKILL
13 by Aaron Bentley
Retrieve messages.
9
import simplejson
4 by Aaron Bentley
Initial test.
10
from StringIO import StringIO
32 by Aaron Bentley
Switch test HTTP server to standard Python logging.
11
import sys
3 by Aaron Bentley
Add test framework.
12
from unittest import TestCase
13 by Aaron Bentley
Retrieve messages.
13
from urlparse import urlparse
15 by Aaron Bentley
Test filtering by message-id.
14
from urlparse import parse_qs
3 by Aaron Bentley
Add test framework.
15
5 by Aaron Bentley
Actual fake service working.
16
from testtools import ExpectedException
17
8 by Aaron Bentley
Test message path.
18
from grackle.client import (
19
    GrackleClient,
21 by Aaron Bentley
Test unsupported orders.
20
    UnsupportedOrder,
8 by Aaron Bentley
Test message path.
21
    )
4 by Aaron Bentley
Initial test.
22
23
22 by Aaron Bentley
Order by thread subject.
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
28 by Aaron Bentley
Extract GrackleStore.
39
class GrackleStore:
34 by Aaron Bentley
Cleanup
40
    """A memory-backed message store."""
5 by Aaron Bentley
Actual fake service working.
41
28 by Aaron Bentley
Extract GrackleStore.
42
    def __init__(self, messages):
34 by Aaron Bentley
Cleanup
43
        """Constructor."""
12 by Aaron Bentley
Use pipe to ensure we only use HTTP once it's running.
44
        self.messages = messages
26 by Aaron Bentley
Implement an archive namespace.
45
46
    def get_messages(self, archive_id, query_string):
34 by Aaron Bentley
Cleanup
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
        """
15 by Aaron Bentley
Test filtering by message-id.
53
        query = parse_qs(query_string)
54
        parameters = simplejson.loads(query['parameters'][0])
22 by Aaron Bentley
Order by thread subject.
55
        order = parameters.get('order')
28 by Aaron Bentley
Extract GrackleStore.
56
        messages = self.messages[archive_id]
22 by Aaron Bentley
Order by thread subject.
57
        if order is not None :
58
            if order not in SUPPORTED_ORDERS:
28 by Aaron Bentley
Extract GrackleStore.
59
                raise UnsupportedOrder
23 by Aaron Bentley
Support thread_oldest order.
60
            elif order.startswith('thread_'):
22 by Aaron Bentley
Order by thread subject.
61
                threaded = threaded_messages(messages)
62
                messages = []
23 by Aaron Bentley
Support thread_oldest order.
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))
24 by Aaron Bentley
Support thread_newest threading.
67
                if order == 'thread_newest':
68
                    threaded.sort(key=lambda t: max(m['date'] for m in t))
22 by Aaron Bentley
Order by thread subject.
69
                for thread in threaded:
70
                    messages.extend(thread)
71
            else:
23 by Aaron Bentley
Support thread_oldest order.
72
                messages.sort(key=lambda m: m[order])
29 by Aaron Bentley
implement include_hidden.
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
19 by Aaron Bentley
Implement memo/limit support.
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]
29 by Aaron Bentley
implement include_hidden.
112
16 by Aaron Bentley
Include next_memo, previous_memo in get_messages response.
113
        response = {
29 by Aaron Bentley
implement include_hidden.
114
            'messages': messages,
19 by Aaron Bentley
Implement memo/limit support.
115
            'next_memo': next_memo,
116
            'previous_memo': previous_memo
16 by Aaron Bentley
Include next_memo, previous_memo in get_messages response.
117
            }
28 by Aaron Bentley
Extract GrackleStore.
118
        return response
119
120
121
122
class ForkedFake:
34 by Aaron Bentley
Cleanup
123
    """A Grackle service fake, as a ContextManager."""
28 by Aaron Bentley
Extract GrackleStore.
124
33 by Aaron Bentley
Cleaner logging switch.
125
    def __init__(self, port, messages=None, write_logs=False):
34 by Aaron Bentley
Cleanup
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
        """
28 by Aaron Bentley
Extract GrackleStore.
133
        self.pid = None
134
        self.port = port
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
135
        if messages is None:
136
            self.messages = {}
137
        else:
138
            self.messages = messages
28 by Aaron Bentley
Extract GrackleStore.
139
        self.read_end, self.write_end = os.pipe()
33 by Aaron Bentley
Cleaner logging switch.
140
        self.write_logs = write_logs
28 by Aaron Bentley
Extract GrackleStore.
141
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
142
    @staticmethod
143
    def from_client(client, messages=None):
34 by Aaron Bentley
Cleanup
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
        """
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
151
        return ForkedFake(client.port, messages)
152
28 by Aaron Bentley
Extract GrackleStore.
153
    def is_ready(self):
34 by Aaron Bentley
Cleanup
154
        """Tell the parent process that the server is ready for writes."""
28 by Aaron Bentley
Extract GrackleStore.
155
        os.write(self.write_end, 'asdf')
156
157
    def __enter__(self):
34 by Aaron Bentley
Cleanup
158
        """Run the service.
159
160
        Fork and start a server in the child.  Return when the server is ready
161
        for use."""
28 by Aaron Bentley
Extract GrackleStore.
162
        pid = os.fork()
163
        if pid == 0:
164
            self.start_server()
165
        self.pid = pid
166
        os.read(self.read_end, 1)
167
        return
168
169
    def start_server(self):
34 by Aaron Bentley
Cleanup
170
        """Start the HTTP server."""
28 by Aaron Bentley
Extract GrackleStore.
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()
33 by Aaron Bentley
Cleaner logging switch.
177
        if self.write_logs:
178
            logging.basicConfig(
179
                stream=sys.stderr, level=logging.INFO)
28 by Aaron Bentley
Extract GrackleStore.
180
        service.serve_forever()
181
182
    def __exit__(self, exc_type, exc_val, traceback):
183
        os.kill(self.pid, SIGKILL)
184
185
186
SUPPORTED_ORDERS = set(
187
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
188
     'thread_subject'])
189
190
191
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
34 by Aaron Bentley
Cleanup
192
    """A request handler that forwards to server.store."""
28 by Aaron Bentley
Extract GrackleStore.
193
32 by Aaron Bentley
Switch test HTTP server to standard Python logging.
194
    def __init__(self, *args, **kwargs):
34 by Aaron Bentley
Cleanup
195
        """Constructor.  Sets up logging."""
32 by Aaron Bentley
Switch test HTTP server to standard Python logging.
196
        self.logger = logging.getLogger('http')
197
        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
198
28 by Aaron Bentley
Extract GrackleStore.
199
    def do_POST(self):
34 by Aaron Bentley
Cleanup
200
        """Create a message on POST."""
28 by Aaron Bentley
Extract GrackleStore.
201
        message = self.rfile.read(int(self.headers['content-length']))
202
        if message == 'This is a message':
203
            self.send_response(httplib.CREATED)
204
            self.end_headers()
205
            self.wfile.close()
206
        else:
207
            self.send_error(httplib.BAD_REQUEST)
208
209
    def do_GET(self):
34 by Aaron Bentley
Cleanup
210
        """Retrieve a list of messages on GET."""
28 by Aaron Bentley
Extract GrackleStore.
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
13 by Aaron Bentley
Retrieve messages.
225
32 by Aaron Bentley
Switch test HTTP server to standard Python logging.
226
    def log_message(self, format, *args):
34 by Aaron Bentley
Cleanup
227
        """Override log_message to use standard Python logging."""
32 by Aaron Bentley
Switch test HTTP server to 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)
231
5 by Aaron Bentley
Actual fake service working.
232
3 by Aaron Bentley
Add test framework.
233
class TestPutMessage(TestCase):
234
235
    def test_put_message(self):
12 by Aaron Bentley
Use pipe to ensure we only use HTTP once it's running.
236
        client = GrackleClient('localhost', 8436)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
237
        with ForkedFake.from_client(client):
7 by Aaron Bentley
Fix URLs etc.
238
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
5 by Aaron Bentley
Actual fake service working.
239
            with ExpectedException(Exception, 'wtf'):
7 by Aaron Bentley
Fix URLs etc.
240
                client.put_message('arch1', 'asdf',
241
                    StringIO('This is not a message'))
11 by Aaron Bentley
Start working on GET.
242
243
244
class TestGetMessages(TestCase):
245
20 by Aaron Bentley
Support order by date
246
    def assertIDOrder(self, ids, messages):
247
        self.assertEqual(ids, [m['message_id'] for m in messages])
248
19 by Aaron Bentley
Implement memo/limit support.
249
    def assertMessageIDs(self, ids, messages):
20 by Aaron Bentley
Support order by date
250
        self.assertIDOrder(
251
            sorted(ids), sorted(messages, key=lambda m:m['message_id']))
19 by Aaron Bentley
Implement memo/limit support.
252
11 by Aaron Bentley
Start working on GET.
253
    def test_get_messages(self):
254
        client = GrackleClient('localhost', 8435)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
255
        with ForkedFake.from_client(client,
11 by Aaron Bentley
Start working on GET.
256
            {'baz':
17 by Aaron Bentley
Switch hyphens to underscores.
257
            [{'message_id': 'foo'},
258
             {'message_id': 'bar'}]}):
15 by Aaron Bentley
Test filtering by message-id.
259
            response = client.get_messages('baz')
17 by Aaron Bentley
Switch hyphens to underscores.
260
        self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
16 by Aaron Bentley
Include next_memo, previous_memo in get_messages response.
261
            response['messages']))
262
        self.assertIs(None, response['next_memo'])
263
        self.assertIs(None, response['previous_memo'])
15 by Aaron Bentley
Test filtering by message-id.
264
265
    def test_get_messages_by_id(self):
266
        client = GrackleClient('localhost', 8437)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
267
        with ForkedFake.from_client(client,
15 by Aaron Bentley
Test filtering by message-id.
268
            {'baz':
17 by Aaron Bentley
Switch hyphens to underscores.
269
            [{'message_id': 'foo'},
270
             {'message_id': 'bar'}]}):
16 by Aaron Bentley
Include next_memo, previous_memo in get_messages response.
271
            response = client.get_messages('baz', message_ids=['foo'])
272
        message, = response['messages']
17 by Aaron Bentley
Switch hyphens to underscores.
273
        self.assertEqual('foo', message['message_id'])
19 by Aaron Bentley
Implement memo/limit support.
274
275
    def test_get_messages_batching(self):
20 by Aaron Bentley
Support order by date
276
        client = GrackleClient('localhost', 8438)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
277
        with ForkedFake.from_client(client,
19 by Aaron Bentley
Implement memo/limit support.
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)
20 by Aaron Bentley
Support order by date
289
22 by Aaron Bentley
Order by thread subject.
290
    def get_messages_member_order_test(self, key):
20 by Aaron Bentley
Support order by date
291
        client = GrackleClient('localhost', 8439)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
292
        with ForkedFake.from_client(client,
22 by Aaron Bentley
Order by thread subject.
293
                {'baz': [{'message_id': 'foo', key: '2011-03-25'},
294
                 {'message_id': 'bar', key: '2011-03-24'}]}):
20 by Aaron Bentley
Support order by date
295
            response = client.get_messages('baz')
296
            self.assertIDOrder(['foo', 'bar'], response['messages'])
22 by Aaron Bentley
Order by thread subject.
297
            response = client.get_messages('baz', order=key)
20 by Aaron Bentley
Support order by date
298
            self.assertIDOrder(['bar', 'foo'], response['messages'])
21 by Aaron Bentley
Test unsupported orders.
299
22 by Aaron Bentley
Order by thread subject.
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)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
311
        with ForkedFake.from_client(client, {'baz': [
22 by Aaron Bentley
Order by thread subject.
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
23 by Aaron Bentley
Support thread_oldest order.
323
    def test_get_messages_thread_oldest_order(self):
324
        client = GrackleClient('localhost', 8439)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
325
        with ForkedFake.from_client(client, {'baz': [
23 by Aaron Bentley
Support thread_oldest order.
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
24 by Aaron Bentley
Support thread_newest threading.
337
    def test_get_messages_thread_newest_order(self):
338
        client = GrackleClient('localhost', 8439)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
339
        with ForkedFake.from_client(client, {'baz': [
24 by Aaron Bentley
Support thread_newest threading.
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
21 by Aaron Bentley
Test unsupported orders.
352
    def test_get_messages_unsupported_order(self):
353
        client = GrackleClient('localhost', 8439)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
354
        with ForkedFake.from_client(client,
21 by Aaron Bentley
Test unsupported orders.
355
                {'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
356
                 {'message_id': 'bar', 'date': '2011-03-24'}]}):
35 by William Grant
Fix test.
357
            with ExpectedException(UnsupportedOrder, ''):
21 by Aaron Bentley
Test unsupported orders.
358
                client.get_messages('baz', order='nonsense')
27 by Aaron Bentley
get_messages supports header parameter.
359
360
    def test_get_messages_headers_no_headers(self):
361
        client = GrackleClient('localhost', 8440)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
362
        with ForkedFake.from_client(client,
27 by Aaron Bentley
get_messages supports header parameter.
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):
29 by Aaron Bentley
implement include_hidden.
373
        client = GrackleClient('localhost', 8441)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
374
        with ForkedFake.from_client(client,
27 by Aaron Bentley
get_messages supports header parameter.
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):
29 by Aaron Bentley
implement include_hidden.
385
        client = GrackleClient('localhost', 8442)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
386
        with ForkedFake.from_client(client,
27 by Aaron Bentley
get_messages supports header parameter.
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'])
28 by Aaron Bentley
Extract GrackleStore.
395
396
    def test_get_messages_max_body_length(self):
29 by Aaron Bentley
implement include_hidden.
397
        client = GrackleClient('localhost', 8443)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
398
        with ForkedFake.from_client(client,
28 by Aaron Bentley
Extract GrackleStore.
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
29 by Aaron Bentley
implement include_hidden.
406
    def test_include_hidden(self):
407
        client = GrackleClient('localhost', 8444)
31 by Aaron Bentley
Switch fake_grackle_service to ForkedFake.from_client.
408
        with ForkedFake.from_client(client,
29 by Aaron Bentley
implement include_hidden.
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