~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Curtis Hovey
  • Date: 2012-02-24 21:43:12 UTC
  • Revision ID: curtis.hovey@canonical.com-20120224214312-zlji369uv0l9v75m
Move errors to their own module.
Remove duplicate definiton of SUPPORTED_DISPLAY_TYPES.

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
    HTTPServer,
3
3
    BaseHTTPRequestHandler,
4
4
    )
 
5
from email.message import Message
 
6
from email.mime.multipart import MIMEMultipart
 
7
from email.mime.text import MIMEText
5
8
import httplib
6
9
import logging
7
10
import os
11
14
import sys
12
15
from unittest import TestCase
13
16
from urlparse import urlparse
14
 
from urlparse import parse_qs
15
17
 
16
18
from testtools import ExpectedException
17
19
 
18
20
from grackle.client import (
19
21
    GrackleClient,
 
22
    UnparsableDateRange,
 
23
    UnsupportedDisplayType,
20
24
    UnsupportedOrder,
21
25
    )
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 (
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
 
class ForkedFake:
 
26
from grackle.store import (
 
27
    MemoryStore,
 
28
    )
 
29
 
 
30
 
 
31
def make_message(message_id, body='body', headers=None, hidden=False):
 
32
    if headers is None:
 
33
        headers = {}
 
34
    headers['Message-Id'] = message_id
 
35
    message = {
 
36
        'message_id': message_id,
 
37
        'headers': headers,
 
38
        'thread_id': message_id,
 
39
        'date': headers.get('date', '2005-01-01'),
 
40
        'subject': headers.get('subject', 'subject'),
 
41
        'author': headers.get('author', 'author'),
 
42
        'hidden': hidden,
 
43
        'attachments': [],
 
44
        'replies': headers.get('in-reply-to', None),
 
45
        'body': body,
 
46
        }
 
47
    return message
 
48
 
 
49
 
 
50
def make_mime_message(message_id, body='body', headers=None, hidden=False,
 
51
                      attachment_type=None):
 
52
    message = MIMEMultipart()
 
53
    message.attach(MIMEText(body))
 
54
    if attachment_type is not None:
 
55
        attachment = Message()
 
56
        attachment.set_payload('attactment data.')
 
57
        attachment['Content-Type'] = attachment_type
 
58
        attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
 
59
        message.attach(attachment)
 
60
    return make_message(message_id, message.get_payload(), headers, hidden)
 
61
 
 
62
 
 
63
class ForkedFakeService:
123
64
    """A Grackle service fake, as a ContextManager."""
124
65
 
125
66
    def __init__(self, port, messages=None, write_logs=False):
126
67
        """Constructor.
127
 
        :param port: The tcp port to use
 
68
 
 
69
        :param port: The tcp port to use.
128
70
        :param messages: A dict of lists of dicts representing messages.  The
129
71
            outer dict represents the archive, the list represents the list of
130
72
            messages for that archive.
141
83
 
142
84
    @staticmethod
143
85
    def from_client(client, messages=None):
144
 
        """Instantiate a ForkedFake from the client.
 
86
        """Instantiate a ForkedFakeService from the client.
145
87
 
146
 
        :param port: The client  to provide service for.
 
88
        :param port: The client to provide service for.
147
89
        :param messages: A dict of lists of dicts representing messages.  The
148
90
            outer dict represents the archive, the list represents the list of
149
91
            messages for that archive.
150
92
        """
151
 
        return ForkedFake(client.port, messages)
 
93
        return ForkedFakeService(client.port, messages)
152
94
 
153
95
    def is_ready(self):
154
96
        """Tell the parent process that the server is ready for writes."""
169
111
    def start_server(self):
170
112
        """Start the HTTP server."""
171
113
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
172
 
        service.store = GrackleStore(self.messages)
 
114
        service.store = MemoryStore(self.messages)
173
115
        for archive_id, messages in service.store.messages.iteritems():
174
116
            for message in messages:
175
117
                message.setdefault('headers', {})
183
125
        os.kill(self.pid, SIGKILL)
184
126
 
185
127
 
186
 
SUPPORTED_ORDERS = set(
187
 
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
188
 
     'thread_subject'])
189
 
 
190
 
 
191
128
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
192
129
    """A request handler that forwards to server.store."""
193
130
 
218
155
                self.send_response(httplib.OK)
219
156
                self.end_headers()
220
157
                self.wfile.write(simplejson.dumps(response))
221
 
            except UnsupportedOrder:
222
 
                self.send_response(httplib.BAD_REQUEST)
223
 
                self.wfile.write('Unsupported order')
 
158
            except Exception, error:
 
159
                self.send_response(
 
160
                    httplib.BAD_REQUEST, error.__doc__)
224
161
                return
225
162
 
226
163
    def log_message(self, format, *args):
227
164
        """Override log_message to use standard Python logging."""
228
165
        message = "%s - - [%s] %s\n" % (
229
 
            self.address_string(), self.log_date_time_string(), format%args)
 
166
            self.address_string(), self.log_date_time_string(), format % args)
230
167
        self.logger.info(message)
231
168
 
232
169
 
233
170
class TestPutMessage(TestCase):
234
171
 
235
172
    def test_put_message(self):
236
 
        client = GrackleClient('localhost', 8436)
237
 
        with ForkedFake.from_client(client):
 
173
        client = GrackleClient('localhost', 8420)
 
174
        with ForkedFakeService.from_client(client):
238
175
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
239
176
            with ExpectedException(Exception, 'wtf'):
240
177
                client.put_message('arch1', 'asdf',
248
185
 
249
186
    def assertMessageIDs(self, ids, messages):
250
187
        self.assertIDOrder(
251
 
            sorted(ids), sorted(messages, key=lambda m:m['message_id']))
 
188
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
252
189
 
253
190
    def test_get_messages(self):
254
 
        client = GrackleClient('localhost', 8435)
255
 
        with ForkedFake.from_client(client,
256
 
            {'baz':
257
 
            [{'message_id': 'foo'},
258
 
             {'message_id': 'bar'}]}):
 
191
        client = GrackleClient('localhost', 8430)
 
192
        archive = {
 
193
            'baz': [make_message('foo'), make_message('bar')]}
 
194
        with ForkedFakeService.from_client(client, archive):
259
195
            response = client.get_messages('baz')
260
196
        self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
261
197
            response['messages']))
264
200
 
265
201
    def test_get_messages_by_id(self):
266
202
        client = GrackleClient('localhost', 8437)
267
 
        with ForkedFake.from_client(client,
268
 
            {'baz':
269
 
            [{'message_id': 'foo'},
270
 
             {'message_id': 'bar'}]}):
 
203
        archive = {
 
204
            'baz': [make_message('foo'), make_message('bar')]}
 
205
        with ForkedFakeService.from_client(client, archive):
271
206
            response = client.get_messages('baz', message_ids=['foo'])
272
207
        message, = response['messages']
273
208
        self.assertEqual('foo', message['message_id'])
274
209
 
275
210
    def test_get_messages_batching(self):
276
211
        client = GrackleClient('localhost', 8438)
277
 
        with ForkedFake.from_client(client,
278
 
            {'baz':
279
 
            [{'message_id': 'foo'},
280
 
             {'message_id': 'bar'}]}):
 
212
        archive = {'baz': [make_message('foo'), make_message('bar')]}
 
213
        with ForkedFakeService.from_client(client, archive):
281
214
            response = client.get_messages('baz', limit=1)
282
215
            self.assertEqual(1, len(response['messages']))
283
216
            messages = response['messages']
289
222
 
290
223
    def get_messages_member_order_test(self, key):
291
224
        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'}]}):
 
225
        archive = {
 
226
            'baz': [
 
227
                make_message('foo', headers={key: '2011-03-25'}),
 
228
                make_message('bar', headers={key: '2011-03-24'}),
 
229
             ]}
 
230
        with ForkedFakeService.from_client(client, archive):
295
231
            response = client.get_messages('baz')
296
232
            self.assertIDOrder(['foo', 'bar'], response['messages'])
297
233
            response = client.get_messages('baz', order=key)
307
243
        self.get_messages_member_order_test('subject')
308
244
 
309
245
    def test_get_messages_thread_subject_order(self):
 
246
        archive = {
 
247
            'baz': [
 
248
                make_message('bar', headers={'subject': 'y'}),
 
249
                make_message('qux', headers={'subject': 'z'}),
 
250
                make_message('foo', headers={'subject': 'x',
 
251
                                             'in-reply-to': 'qux'}),
 
252
             ]}
310
253
        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
 
            ]}):
 
254
        with ForkedFakeService.from_client(client, archive):
316
255
            response = client.get_messages('baz')
317
256
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
318
257
            response = client.get_messages('baz', order='subject')
322
261
 
323
262
    def test_get_messages_thread_oldest_order(self):
324
263
        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
 
            ]}):
 
264
        archive = {
 
265
            'baz': [
 
266
                make_message('bar', headers={'date': 'x'}),
 
267
                make_message('qux', headers={'date': 'z'}),
 
268
                make_message('foo', headers={'date': 'y',
 
269
                                             'in-reply-to': 'qux'}),
 
270
            ]}
 
271
        with ForkedFakeService.from_client(client, archive):
330
272
            response = client.get_messages('baz')
331
273
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
332
274
            response = client.get_messages('baz', order='date')
336
278
 
337
279
    def test_get_messages_thread_newest_order(self):
338
280
        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
 
            ]}):
 
281
        archive = {
 
282
            'baz': [
 
283
                make_message('bar', headers={'date': 'x'}),
 
284
                make_message('qux', headers={'date': 'w'}),
 
285
                make_message('foo', headers={'date': 'y',
 
286
                                             'in-reply-to': 'bar'}),
 
287
                make_message('baz', headers={'date': 'z',
 
288
                                             'in-reply-to': 'qux'}),
 
289
            ]}
 
290
        with ForkedFakeService.from_client(client, archive):
345
291
            response = client.get_messages('baz', order='date')
346
292
            self.assertIDOrder(
347
293
                ['qux', 'bar', 'foo', 'baz'], response['messages'])
351
297
 
352
298
    def test_get_messages_unsupported_order(self):
353
299
        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'}]}):
 
300
        archive = {
 
301
            'baz': [
 
302
                make_message('foo', headers={'date': '2011-03-25'}),
 
303
                make_message('foo', headers={'date': '2011-03-24'}),
 
304
            ]}
 
305
        with ForkedFakeService.from_client(client, archive):
357
306
            with ExpectedException(UnsupportedOrder, ''):
358
307
                client.get_messages('baz', order='nonsense')
359
308
 
360
309
    def test_get_messages_headers_no_headers(self):
361
310
        client = GrackleClient('localhost', 8440)
362
 
        with ForkedFake.from_client(client,
363
 
            {'baz': [
364
 
                {'message_id': 'foo'}
365
 
            ]}):
 
311
        archive = {'baz': [make_message('foo')]}
 
312
        with ForkedFakeService.from_client(client, archive):
366
313
            response = client.get_messages('baz', headers=[
367
314
                'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
368
315
        first_message = response['messages'][0]
371
318
 
372
319
    def test_get_messages_headers_exclude_headers(self):
373
320
        client = GrackleClient('localhost', 8441)
374
 
        with ForkedFake.from_client(client,
375
 
            {'baz': [
376
 
                {'message_id': 'foo', 'headers': {'From': 'me'}}
377
 
            ]}):
 
321
        archive = {
 
322
            'baz': [make_message('foo', headers={'From': 'me'})]}
 
323
        with ForkedFakeService.from_client(client, archive):
378
324
            response = client.get_messages('baz', headers=[
379
325
                'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
380
326
        first_message = response['messages'][0]
383
329
 
384
330
    def test_get_messages_headers_include_headers(self):
385
331
        client = GrackleClient('localhost', 8442)
386
 
        with ForkedFake.from_client(client,
387
 
            {'baz': [
388
 
                {'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
389
 
            ]}):
 
332
        archive = {
 
333
            'baz': [
 
334
                make_message('foo', headers={'From': 'me', 'To': 'you'})]}
 
335
        with ForkedFakeService.from_client(client, archive):
390
336
            response = client.get_messages('baz', headers=[
391
337
                'From', 'To'])
392
338
        first_message = response['messages'][0]
395
341
 
396
342
    def test_get_messages_max_body_length(self):
397
343
        client = GrackleClient('localhost', 8443)
398
 
        with ForkedFake.from_client(client,
399
 
            {'baz': [
400
 
                {'message_id': 'foo', 'body': u'abcdefghi'}
401
 
            ]}):
 
344
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
 
345
        with ForkedFakeService.from_client(client, archive):
402
346
            response = client.get_messages('baz', max_body_length=3)
403
347
        first_message = response['messages'][0]
404
348
        self.assertEqual('abc', first_message['body'])
405
349
 
406
350
    def test_include_hidden(self):
407
351
        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
 
            ]}):
 
352
        archive = {
 
353
            'baz': [
 
354
                make_message('foo', hidden=True),
 
355
                make_message('bar', hidden=False),
 
356
            ]}
 
357
        with ForkedFakeService.from_client(client, archive):
413
358
            response = client.get_messages('baz', include_hidden=True)
414
359
            self.assertMessageIDs(['bar', 'foo'], response['messages'])
415
360
            response = client.get_messages('baz', include_hidden=False)
416
361
            self.assertMessageIDs(['bar'], response['messages'])
417
362
 
 
363
    def test_display_type_unknown_value(self):
 
364
        client = GrackleClient('localhost', 8445)
 
365
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
 
366
        with ForkedFakeService.from_client(client, archive):
 
367
            with ExpectedException(UnsupportedDisplayType, ''):
 
368
                client.get_messages('baz', display_type='unknown')
 
369
 
 
370
    def test_display_type_headers_only(self):
 
371
        client = GrackleClient('localhost', 8446)
 
372
        archive = {
 
373
            'baz': [
 
374
                make_message('foo', body=u'abcdefghi',
 
375
                             headers={'From': 'me', 'To': 'you'})]}
 
376
        with ForkedFakeService.from_client(client, archive):
 
377
            response = client.get_messages('baz', display_type='headers-only')
 
378
        first_message = response['messages'][0]
 
379
        self.assertEqual('foo', first_message['message_id'])
 
380
        self.assertEqual(
 
381
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
 
382
            first_message['headers'])
 
383
        self.assertNotIn('body', first_message)
 
384
 
 
385
    def test_display_type_text_only(self):
 
386
        client = GrackleClient('localhost', 8446)
 
387
        archive = {
 
388
            'baz': [
 
389
                make_mime_message(
 
390
                    'foo', 'abcdefghi',
 
391
                    headers={'From': 'me', 'To': 'you'},
 
392
                    attachment_type='text/x-diff')]}
 
393
        with ForkedFakeService.from_client(client, archive):
 
394
            response = client.get_messages('baz', display_type='text-only')
 
395
        first_message = response['messages'][0]
 
396
        self.assertEqual('foo', first_message['message_id'])
 
397
        self.assertEqual('me', first_message['headers']['From'])
 
398
        self.assertEqual('you', first_message['headers']['To'])
 
399
        self.assertEqual('abcdefghi', first_message['body'])
 
400
 
 
401
    def test_display_type_all(self):
 
402
        client = GrackleClient('localhost', 8447)
 
403
        archive = {
 
404
            'baz': [
 
405
                make_mime_message(
 
406
                    'foo', 'abcdefghi',
 
407
                    headers={'From': 'me', 'To': 'you'},
 
408
                    attachment_type='text/x-diff')]}
 
409
        with ForkedFakeService.from_client(client, archive):
 
410
            response = client.get_messages('baz', display_type='all')
 
411
        first_message = response['messages'][0]
 
412
        self.assertEqual('foo', first_message['message_id'])
 
413
        self.assertEqual('me', first_message['headers']['From'])
 
414
        self.assertEqual('you', first_message['headers']['To'])
 
415
        self.assertEqual(
 
416
            'abcdefghi\n\nattactment data.', first_message['body'])
 
417
 
 
418
    def test_date_range(self):
 
419
        client = GrackleClient('localhost', 8448)
 
420
        archive = {
 
421
            'baz': [
 
422
                make_mime_message(
 
423
                    'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
 
424
                make_mime_message(
 
425
                    'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
 
426
                make_mime_message(
 
427
                    'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
 
428
                make_mime_message(
 
429
                    'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
 
430
                make_mime_message(
 
431
                    'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
 
432
                    ]}
 
433
        with ForkedFakeService.from_client(client, archive):
 
434
            response = client.get_messages(
 
435
                'baz', date_range='2012-01-01..2012-01-31')
 
436
        ids = sorted(m['message_id'] for m in response['messages'])
 
437
        self.assertEqual(['bar', 'naf', 'qux'], ids)
 
438
 
 
439
    def test_date_range_unparsabledaterange(self):
 
440
        client = GrackleClient('localhost', 8449)
 
441
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
 
442
        with ForkedFakeService.from_client(client, archive):
 
443
            with ExpectedException(UnparsableDateRange, ''):
 
444
                client.get_messages('baz', date_range='2012-01-01')
 
445
 
 
446
    def test_date_range_unparsabledaterange_missing_part(self):
 
447
        client = GrackleClient('localhost', 8450)
 
448
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
 
449
        with ForkedFakeService.from_client(client, archive):
 
450
            with ExpectedException(UnparsableDateRange, ''):
 
451
                client.get_messages('baz', date_range='2012-01-01..')
 
452
 
 
453
    def test_date_range_unparsabledaterange_extra_part(self):
 
454
        client = GrackleClient('localhost', 8451)
 
455
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
 
456
        with ForkedFakeService.from_client(client, archive):
 
457
            with ExpectedException(UnparsableDateRange, ''):
 
458
                client.get_messages('baz', date_range='2012-01..12-02..12-03')