~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Curtis Hovey
  • Date: 2012-02-29 23:55:28 UTC
  • Revision ID: curtis.hovey@canonical.com-20120229235528-7wphq02b2y9vt7ni
Implemented a partial put into the MemoryStore.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
import sys
15
15
from unittest import TestCase
16
16
from urlparse import urlparse
17
 
from urlparse import parse_qs
18
17
 
19
18
from testtools import ExpectedException
20
19
 
24
23
    UnsupportedDisplayType,
25
24
    UnsupportedOrder,
26
25
    )
 
26
from grackle.store import (
 
27
    MemoryStore,
 
28
    )
27
29
 
28
30
 
29
31
def make_message(message_id, body='body', headers=None, hidden=False):
58
60
    return make_message(message_id, message.get_payload(), headers, hidden)
59
61
 
60
62
 
61
 
def threaded_messages(messages):
62
 
    threads = {}
63
 
    count = 0
64
 
    pending = []
65
 
    for message in messages:
66
 
        if message.get('replies') is None:
67
 
            threads[message['message_id']] = [message]
68
 
            count += 1
69
 
        else:
70
 
            pending.append(message)
71
 
    for message in pending:
72
 
        threads[message['replies']].append(message)
73
 
    return threads.values()
74
 
 
75
 
 
76
 
class GrackleStore:
77
 
    """A memory-backed message store."""
78
 
 
79
 
    def __init__(self, messages):
80
 
        """Constructor."""
81
 
        self.messages = messages
82
 
 
83
 
    @staticmethod
84
 
    def is_multipart(message):
85
 
        return isinstance(message['body'], list)
86
 
 
87
 
    def get_messages(self, archive_id, query_string):
88
 
        """Return matching messages.
89
 
 
90
 
        :param archive_id: The archive to retrieve from.
91
 
        :param query_string: Contains 'parameters', which is a JSON-format
92
 
            string describing parameters.
93
 
        """
94
 
        query = parse_qs(query_string)
95
 
        parameters = simplejson.loads(query['parameters'][0])
96
 
        order = parameters.get('order')
97
 
        messages = self.messages[archive_id]
98
 
        if order is not None:
99
 
            if order not in SUPPORTED_ORDERS:
100
 
                raise UnsupportedOrder
101
 
            elif order.startswith('thread_'):
102
 
                threaded = threaded_messages(messages)
103
 
                messages = []
104
 
                if order == 'thread_subject':
105
 
                    threaded.sort(key=lambda t: t[0]['subject'])
106
 
                if order == 'thread_oldest':
107
 
                    threaded.sort(key=lambda t: min(m['date'] for m in t))
108
 
                if order == 'thread_newest':
109
 
                    threaded.sort(key=lambda t: max(m['date'] for m in t))
110
 
                for thread in threaded:
111
 
                    messages.extend(thread)
112
 
            else:
113
 
                messages.sort(key=lambda m: m[order])
114
 
        display_type = parameters.get('display_type', 'all')
115
 
        if display_type not in SUPPORTED_DISPLAY_TYPES:
116
 
            raise UnsupportedDisplayType
117
 
        if 'date_range' in parameters:
118
 
            try:
119
 
                start_date, end_date = parameters['date_range'].split('..')
120
 
                if not start_date or not end_date:
121
 
                    raise UnparsableDateRange
122
 
            except ValueError:
123
 
                raise UnparsableDateRange
124
 
        new_messages = []
125
 
        for message in messages:
126
 
            if (not parameters['include_hidden'] and message['hidden']):
127
 
                continue
128
 
            if ('message_ids' in parameters
129
 
                and message['message_id'] not in parameters['message_ids']):
130
 
                continue
131
 
            if ('date_range' in parameters
132
 
                and (message['date'] < start_date
133
 
                     or message['date'] > end_date)):
134
 
                continue
135
 
            message = dict(message)
136
 
            if 'headers' in parameters:
137
 
                headers = dict(
138
 
                    (k, v) for k, v in message['headers'].iteritems()
139
 
                    if k in parameters['headers'])
140
 
                message['headers'] = headers
141
 
            if display_type == 'headers-only':
142
 
                del message['body']
143
 
            elif display_type == 'text-only' and self.is_multipart(message):
144
 
                text_parts = [
145
 
                    part.get_payload() for part in message['body']
146
 
                    if part.get_content_type() == 'text/plain']
147
 
                message['body'] = '\n\n'.join(text_parts)
148
 
            elif display_type == 'all' and self.is_multipart(message):
149
 
                parts = [str(part.get_payload()) for part in message['body']]
150
 
                message['body'] = '\n\n'.join(parts)
151
 
            max_body = parameters.get('max_body_length')
152
 
            if max_body is not None and display_type != 'headers-only':
153
 
                message['body'] = message['body'][:max_body]
154
 
            new_messages.append(message)
155
 
        messages = new_messages
156
 
        limit = parameters.get('limit', 100)
157
 
        memo = parameters.get('memo')
158
 
        message_id_indices = dict(
159
 
            (m['message_id'], idx) for idx, m in enumerate(messages))
160
 
        if memo is None:
161
 
            start = 0
162
 
        else:
163
 
            start = message_id_indices[memo.encode('rot13')]
164
 
        if start > 0:
165
 
            previous_memo = messages[start - 1]['message_id'].encode('rot13')
166
 
        else:
167
 
            previous_memo = None
168
 
        end = min(start + limit, len(messages))
169
 
        if end < len(messages):
170
 
            next_memo = messages[end]['message_id'].encode('rot13')
171
 
        else:
172
 
            next_memo = None
173
 
        messages = messages[start:end]
174
 
 
175
 
        response = {
176
 
            'messages': messages,
177
 
            'next_memo': next_memo,
178
 
            'previous_memo': previous_memo
179
 
            }
180
 
        return response
181
 
 
182
 
 
183
63
class ForkedFakeService:
184
64
    """A Grackle service fake, as a ContextManager."""
185
65
 
186
 
    def __init__(self, port, messages=None, write_logs=False):
 
66
    def __init__(self, port, message_archives=None, write_logs=False):
187
67
        """Constructor.
188
68
 
189
69
        :param port: The tcp port to use.
190
 
        :param messages: A dict of lists of dicts representing messages.  The
191
 
            outer dict represents the archive, the list represents the list of
192
 
            messages for that archive.
 
70
        :param message_archives: A dict of lists of dicts representing
 
71
            archives of messages. The outer dict represents the archive,
 
72
            the list represents the list of messages for that archive.
193
73
        :param write_logs: If true, log messages will be written to stdout.
194
74
        """
195
75
        self.pid = None
196
76
        self.port = port
197
 
        if messages is None:
198
 
            self.messages = {}
 
77
        if message_archives is None:
 
78
            self.message_archives = {}
199
79
        else:
200
 
            self.messages = messages
 
80
            self.message_archives = message_archives
201
81
        self.read_end, self.write_end = os.pipe()
202
82
        self.write_logs = write_logs
203
83
 
204
84
    @staticmethod
205
 
    def from_client(client, messages=None):
 
85
    def from_client(client, message_archives=None):
206
86
        """Instantiate a ForkedFakeService from the client.
207
87
 
208
88
        :param port: The client to provide service for.
209
 
        :param messages: A dict of lists of dicts representing messages.  The
210
 
            outer dict represents the archive, the list represents the list of
211
 
            messages for that archive.
 
89
        :param message_archives: A dict of lists of dicts representing
 
90
            archives of messages. The outer dict represents the archive,
 
91
            the list represents the list of messages for that archive.
212
92
        """
213
 
        return ForkedFakeService(client.port, messages)
 
93
        return ForkedFakeService(client.port, message_archives)
214
94
 
215
95
    def is_ready(self):
216
96
        """Tell the parent process that the server is ready for writes."""
231
111
    def start_server(self):
232
112
        """Start the HTTP server."""
233
113
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
234
 
        service.store = GrackleStore(self.messages)
235
 
        for archive_id, messages in service.store.messages.iteritems():
 
114
        service.store = MemoryStore(self.message_archives)
 
115
        for archive_id, messages in service.store.message_archives.iteritems():
236
116
            for message in messages:
237
117
                message.setdefault('headers', {})
238
118
        self.is_ready()
245
125
        os.kill(self.pid, SIGKILL)
246
126
 
247
127
 
248
 
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
249
 
 
250
 
 
251
 
SUPPORTED_ORDERS = set(
252
 
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
253
 
     'thread_subject'])
254
 
 
255
 
 
256
128
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
257
129
    """A request handler that forwards to server.store."""
258
130
 
264
136
    def do_POST(self):
265
137
        """Create a message on POST."""
266
138
        message = self.rfile.read(int(self.headers['content-length']))
 
139
        scheme, netloc, path, params, query_string, fragments = (
 
140
            urlparse(self.path))
 
141
        parts = path.split('/')
 
142
        if parts[1] == 'archive':
 
143
            # This expected path is /archive/archive_id/message_id.
 
144
            self.server.store.put_message(parts[2], parts[3], message)
267
145
        if message == 'This is a message':
268
146
            self.send_response(httplib.CREATED)
269
147
            self.end_headers()
298
176
class TestPutMessage(TestCase):
299
177
 
300
178
    def test_put_message(self):
301
 
        client = GrackleClient('localhost', 8436)
302
 
        with ForkedFakeService.from_client(client):
303
 
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
 
179
        client = GrackleClient('localhost', 8420)
 
180
        message_archives = {'arch1': []}
 
181
        service = ForkedFakeService.from_client(client, message_archives)
 
182
        with service:
 
183
            client.put_message('arch1', 'id1', StringIO('This is a message'))
 
184
            response = client.get_messages('arch1')
 
185
            self.assertEqual(1, len(response['messages']))
 
186
            message = response['messages'][0]
 
187
            self.assertEqual('id1', message['message_id'])
 
188
 
 
189
    def test_put_message_without_message(self):
 
190
        client = GrackleClient('localhost', 8421)
 
191
        message_archives = {'arch1': []}
 
192
        with ForkedFakeService.from_client(client, message_archives):
304
193
            with ExpectedException(Exception, 'wtf'):
305
 
                client.put_message('arch1', 'asdf',
 
194
                client.put_message('arch1', 'id1',
306
195
                    StringIO('This is not a message'))
307
196
 
308
197
 
316
205
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
317
206
 
318
207
    def test_get_messages(self):
319
 
        client = GrackleClient('localhost', 8435)
 
208
        client = GrackleClient('localhost', 8430)
320
209
        archive = {
321
210
            'baz': [make_message('foo'), make_message('bar')]}
322
211
        with ForkedFakeService.from_client(client, archive):