~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
8
9
import simplejson
9
10
from StringIO import StringIO
 
11
import sys
10
12
from unittest import TestCase
11
13
from urlparse import urlparse
12
14
from urlparse import parse_qs
34
36
    return threads.values()
35
37
 
36
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
 
37
122
class ForkedFake:
 
123
    """A Grackle service fake, as a ContextManager."""
38
124
 
39
 
    def __init__(self, port, messages=None):
 
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
        """
40
133
        self.pid = None
41
134
        self.port = port
42
 
        self.messages = messages
 
135
        if messages is None:
 
136
            self.messages = {}
 
137
        else:
 
138
            self.messages = messages
43
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)
44
152
 
45
153
    def is_ready(self):
 
154
        """Tell the parent process that the server is ready for writes."""
46
155
        os.write(self.write_end, 'asdf')
47
156
 
48
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."""
49
162
        pid = os.fork()
50
163
        if pid == 0:
51
164
            self.start_server()
54
167
        return
55
168
 
56
169
    def start_server(self):
 
170
        """Start the HTTP server."""
57
171
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
58
 
        service.messages = self.messages
59
 
        for archive_id, messages in service.messages.iteritems():
 
172
        service.store = GrackleStore(self.messages)
 
173
        for archive_id, messages in service.store.messages.iteritems():
60
174
            for message in messages:
61
175
                message.setdefault('headers', {})
62
176
        self.is_ready()
 
177
        if self.write_logs:
 
178
            logging.basicConfig(
 
179
                stream=sys.stderr, level=logging.INFO)
63
180
        service.serve_forever()
64
181
 
65
182
    def __exit__(self, exc_type, exc_val, traceback):
72
189
 
73
190
 
74
191
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)
75
198
 
76
199
    def do_POST(self):
 
200
        """Create a message on POST."""
77
201
        message = self.rfile.read(int(self.headers['content-length']))
78
202
        if message == 'This is a message':
79
203
            self.send_response(httplib.CREATED)
83
207
            self.send_error(httplib.BAD_REQUEST)
84
208
 
85
209
    def do_GET(self):
 
210
        """Retrieve a list of messages on GET."""
86
211
        scheme, netloc, path, params, query_string, fragments = (
87
212
            urlparse(self.path))
88
213
        parts = path.split('/')
89
214
        if parts[1] == 'archive':
90
 
            self.get_messages(parts[2], query_string)
91
 
 
92
 
    def get_messages(self, archive_id, query_string):
93
 
        query = parse_qs(query_string)
94
 
        parameters = simplejson.loads(query['parameters'][0])
95
 
        order = parameters.get('order')
96
 
        messages = self.server.messages[archive_id]
97
 
        if order is not None :
98
 
            if order not in SUPPORTED_ORDERS:
 
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:
99
222
                self.send_response(httplib.BAD_REQUEST)
100
223
                self.wfile.write('Unsupported order')
101
224
                return
102
 
            elif order.startswith('thread_'):
103
 
                threaded = threaded_messages(messages)
104
 
                messages = []
105
 
                if order == 'thread_subject':
106
 
                    threaded.sort(key=lambda t: t[0]['subject'])
107
 
                if order == 'thread_oldest':
108
 
                    threaded.sort(key=lambda t: min(m['date'] for m in t))
109
 
                if order == 'thread_newest':
110
 
                    threaded.sort(key=lambda t: max(m['date'] for m in t))
111
 
                for thread in threaded:
112
 
                    messages.extend(thread)
113
 
            else:
114
 
                messages.sort(key=lambda m: m[order])
115
 
        messages = [m for m in messages
116
 
                    if 'message_ids' not in parameters or
117
 
                    m['message_id'] in parameters['message_ids']]
118
 
        self.send_response(httplib.OK)
119
 
        self.end_headers()
120
 
        limit = parameters.get('limit', 100)
121
 
        memo = parameters.get('memo')
122
 
        message_id_indices = dict(
123
 
            (m['message_id'], idx) for idx, m in enumerate(messages))
124
 
        if memo is None:
125
 
            start = 0
126
 
        else:
127
 
            start = message_id_indices[memo.encode('rot13')]
128
 
        if start > 0:
129
 
            previous_memo = messages[start - 1]['message_id'].encode('rot13')
130
 
        else:
131
 
            previous_memo = None
132
 
        end = min(start + limit, len(messages))
133
 
        if end < len(messages):
134
 
            next_memo = messages[end]['message_id'].encode('rot13')
135
 
        else:
136
 
            next_memo = None
137
 
        messages = messages[start:end]
138
 
        new_messages = []
139
 
        for message in messages:
140
 
            message = dict(message)
141
 
            if 'headers' in parameters:
142
 
                headers = dict(
143
 
                    (k, v) for k, v in message['headers'].iteritems()
144
 
                    if k in parameters['headers'])
145
 
                message['headers'] = headers
146
 
            new_messages.append(message)
147
 
        response = {
148
 
            'messages': new_messages,
149
 
            'next_memo': next_memo,
150
 
            'previous_memo': previous_memo
151
 
            }
152
 
        self.wfile.write(simplejson.dumps(response))
153
 
 
154
 
 
155
 
def fake_grackle_service(client, messages=None):
156
 
    if messages is None:
157
 
        messages = {}
158
 
    return ForkedFake(client.port, messages)
 
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)
159
231
 
160
232
 
161
233
class TestPutMessage(TestCase):
162
234
 
163
235
    def test_put_message(self):
164
236
        client = GrackleClient('localhost', 8436)
165
 
        with fake_grackle_service(client):
 
237
        with ForkedFake.from_client(client):
166
238
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
167
239
            with ExpectedException(Exception, 'wtf'):
168
240
                client.put_message('arch1', 'asdf',
180
252
 
181
253
    def test_get_messages(self):
182
254
        client = GrackleClient('localhost', 8435)
183
 
        with fake_grackle_service(client,
 
255
        with ForkedFake.from_client(client,
184
256
            {'baz':
185
257
            [{'message_id': 'foo'},
186
258
             {'message_id': 'bar'}]}):
192
264
 
193
265
    def test_get_messages_by_id(self):
194
266
        client = GrackleClient('localhost', 8437)
195
 
        with fake_grackle_service(client,
 
267
        with ForkedFake.from_client(client,
196
268
            {'baz':
197
269
            [{'message_id': 'foo'},
198
270
             {'message_id': 'bar'}]}):
202
274
 
203
275
    def test_get_messages_batching(self):
204
276
        client = GrackleClient('localhost', 8438)
205
 
        with fake_grackle_service(client,
 
277
        with ForkedFake.from_client(client,
206
278
            {'baz':
207
279
            [{'message_id': 'foo'},
208
280
             {'message_id': 'bar'}]}):
217
289
 
218
290
    def get_messages_member_order_test(self, key):
219
291
        client = GrackleClient('localhost', 8439)
220
 
        with fake_grackle_service(client,
 
292
        with ForkedFake.from_client(client,
221
293
                {'baz': [{'message_id': 'foo', key: '2011-03-25'},
222
294
                 {'message_id': 'bar', key: '2011-03-24'}]}):
223
295
            response = client.get_messages('baz')
236
308
 
237
309
    def test_get_messages_thread_subject_order(self):
238
310
        client = GrackleClient('localhost', 8439)
239
 
        with fake_grackle_service(client, {'baz': [
 
311
        with ForkedFake.from_client(client, {'baz': [
240
312
            {'message_id': 'bar', 'subject': 'y'},
241
313
            {'message_id': 'qux', 'subject': 'z'},
242
314
            {'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
250
322
 
251
323
    def test_get_messages_thread_oldest_order(self):
252
324
        client = GrackleClient('localhost', 8439)
253
 
        with fake_grackle_service(client, {'baz': [
 
325
        with ForkedFake.from_client(client, {'baz': [
254
326
            {'message_id': 'bar', 'date': 'x'},
255
327
            {'message_id': 'qux', 'date': 'z'},
256
328
            {'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
264
336
 
265
337
    def test_get_messages_thread_newest_order(self):
266
338
        client = GrackleClient('localhost', 8439)
267
 
        with fake_grackle_service(client, {'baz': [
 
339
        with ForkedFake.from_client(client, {'baz': [
268
340
            {'message_id': 'bar', 'date': 'x'},
269
341
            {'message_id': 'qux', 'date': 'w'},
270
342
            {'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
279
351
 
280
352
    def test_get_messages_unsupported_order(self):
281
353
        client = GrackleClient('localhost', 8439)
282
 
        with fake_grackle_service(client,
 
354
        with ForkedFake.from_client(client,
283
355
                {'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
284
356
                 {'message_id': 'bar', 'date': '2011-03-24'}]}):
285
 
            with ExpectedException(UnsupportedOrder):
 
357
            with ExpectedException(UnsupportedOrder, ''):
286
358
                client.get_messages('baz', order='nonsense')
287
359
 
288
360
    def test_get_messages_headers_no_headers(self):
289
361
        client = GrackleClient('localhost', 8440)
290
 
        with fake_grackle_service(client,
 
362
        with ForkedFake.from_client(client,
291
363
            {'baz': [
292
364
                {'message_id': 'foo'}
293
365
            ]}):
298
370
        self.assertEqual({}, first_message['headers'])
299
371
 
300
372
    def test_get_messages_headers_exclude_headers(self):
301
 
        client = GrackleClient('localhost', 8440)
302
 
        with fake_grackle_service(client,
 
373
        client = GrackleClient('localhost', 8441)
 
374
        with ForkedFake.from_client(client,
303
375
            {'baz': [
304
376
                {'message_id': 'foo', 'headers': {'From': 'me'}}
305
377
            ]}):
310
382
        self.assertEqual({}, first_message['headers'])
311
383
 
312
384
    def test_get_messages_headers_include_headers(self):
313
 
        client = GrackleClient('localhost', 8440)
314
 
        with fake_grackle_service(client,
 
385
        client = GrackleClient('localhost', 8442)
 
386
        with ForkedFake.from_client(client,
315
387
            {'baz': [
316
388
                {'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
317
389
            ]}):
320
392
        first_message = response['messages'][0]
321
393
        self.assertEqual('foo', first_message['message_id'])
322
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