/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
1
# Copyright (C) 2006, 2007 Canonical Ltd
1752.2.30 by Martin Pool
Start adding a RemoteBzrDir, etc
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
17
"""Tests for remote bzrdir/branch/repo/etc
18
19
These are proxy objects which act on remote objects by sending messages
20
through a smart client.  The proxies are to be created when attempting to open
21
the object given a transport that supports smartserver rpc operations. 
22
"""
23
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
24
from cStringIO import StringIO
25
2018.5.78 by Andrew Bennetts
Implement RemoteRepository.lock_write/unlock to expect and send tokens over the
26
from bzrlib import (
27
    bzrdir,
28
    errors,
29
    remote,
30
    tests,
31
    )
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
32
from bzrlib.branch import Branch
33
from bzrlib.bzrdir import BzrDir, BzrDirFormat
34
from bzrlib.remote import (
35
    RemoteBranch,
36
    RemoteBzrDir,
37
    RemoteBzrDirFormat,
2018.5.57 by Robert Collins
Implement RemoteRepository.is_shared (Robert Collins, Vincent Ladeuil).
38
    RemoteRepository,
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
39
    )
40
from bzrlib.revision import NULL_REVISION
41
from bzrlib.smart import server
42
from bzrlib.smart.client import SmartClient
2018.5.20 by Andrew Bennetts
Move bzrlib/transport/smart/_smart.py to bzrlib/transport/remote.py and rename SmartTransport to RemoteTransport (Robert Collins, Andrew Bennetts)
43
from bzrlib.transport import remote as remote_transport
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
44
from bzrlib.transport.memory import MemoryTransport
1752.2.30 by Martin Pool
Start adding a RemoteBzrDir, etc
45
2018.5.24 by Andrew Bennetts
Setting NO_SMART_VFS in environment will disable VFS methods in the smart server. (Robert Collins, John Arbash Meinel, Andrew Bennetts)
46
2018.5.42 by Robert Collins
Various hopefully improvements, but wsgi is broken, handing over to spiv :).
47
class BasicRemoteObjectTests(tests.TestCaseWithTransport):
1752.2.30 by Martin Pool
Start adding a RemoteBzrDir, etc
48
49
    def setUp(self):
2018.5.42 by Robert Collins
Various hopefully improvements, but wsgi is broken, handing over to spiv :).
50
        super(BasicRemoteObjectTests, self).setUp()
51
        self.transport_server = server.SmartTCPServer_for_testing
52
        self.transport = self.get_transport()
1752.2.30 by Martin Pool
Start adding a RemoteBzrDir, etc
53
        self.client = self.transport.get_smart_client()
54
        # make a branch that can be opened over the smart transport
1752.2.31 by Martin Pool
[broken] some support for write operations over hpss
55
        self.local_wt = BzrDir.create_standalone_workingtree('.')
1752.2.30 by Martin Pool
Start adding a RemoteBzrDir, etc
56
57
    def test_create_remote_bzrdir(self):
58
        b = remote.RemoteBzrDir(self.transport)
59
        self.assertIsInstance(b, BzrDir)
60
61
    def test_open_remote_branch(self):
2018.6.1 by Robert Collins
Implement a BzrDir.open_branch smart server method for opening a branch without VFS.
62
        # open a standalone branch in the working directory
1752.2.30 by Martin Pool
Start adding a RemoteBzrDir, etc
63
        b = remote.RemoteBzrDir(self.transport)
64
        branch = b.open_branch()
65
1752.2.31 by Martin Pool
[broken] some support for write operations over hpss
66
    def test_remote_repository(self):
67
        b = BzrDir.open_from_transport(self.transport)
68
        repo = b.open_repository()
2018.5.40 by Robert Collins
Implement a remote Repository.has_revision method.
69
        revid = u'\xc823123123'
70
        self.assertFalse(repo.has_revision(revid))
71
        self.local_wt.commit(message='test commit', rev_id=revid)
72
        self.assertTrue(repo.has_revision(revid))
1752.2.31 by Martin Pool
[broken] some support for write operations over hpss
73
74
    def test_remote_branch_revision_history(self):
75
        b = BzrDir.open_from_transport(self.transport).open_branch()
2018.5.38 by Robert Collins
Implement RemoteBranch.revision_history().
76
        self.assertEqual([], b.revision_history())
77
        r1 = self.local_wt.commit('1st commit')
78
        r2 = self.local_wt.commit('1st commit', rev_id=u'\xc8')
79
        self.assertEqual([r1, r2], b.revision_history())
1752.2.31 by Martin Pool
[broken] some support for write operations over hpss
80
1752.2.30 by Martin Pool
Start adding a RemoteBzrDir, etc
81
    def test_find_correct_format(self):
2018.5.20 by Andrew Bennetts
Move bzrlib/transport/smart/_smart.py to bzrlib/transport/remote.py and rename SmartTransport to RemoteTransport (Robert Collins, Andrew Bennetts)
82
        """Should open a RemoteBzrDir over a RemoteTransport"""
1752.2.30 by Martin Pool
Start adding a RemoteBzrDir, etc
83
        fmt = BzrDirFormat.find_format(self.transport)
2018.5.25 by Andrew Bennetts
Make sure RemoteBzrDirFormat is always registered (John Arbash Meinel, Robert Collins, Andrew Bennetts).
84
        self.assertTrue(RemoteBzrDirFormat in BzrDirFormat._control_formats)
1752.2.30 by Martin Pool
Start adding a RemoteBzrDir, etc
85
        self.assertIsInstance(fmt, remote.RemoteBzrDirFormat)
86
87
    def test_open_detected_smart_format(self):
88
        fmt = BzrDirFormat.find_format(self.transport)
89
        d = fmt.open(self.transport)
90
        self.assertIsInstance(d, BzrDir)
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
91
92
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
93
class FakeProtocol(object):
94
    """Lookalike SmartClientRequestProtocolOne allowing body reading tests."""
95
96
    def __init__(self, body):
97
        self._body_buffer = StringIO(body)
98
99
    def read_body_bytes(self, count=-1):
100
        return self._body_buffer.read(count)
101
102
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
103
class FakeClient(SmartClient):
104
    """Lookalike for SmartClient allowing testing."""
105
    
106
    def __init__(self, responses):
107
        # We don't call the super init because there is no medium.
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
108
        """create a FakeClient.
109
110
        :param respones: A list of response-tuple, body-data pairs to be sent
111
            back to callers.
112
        """
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
113
        self.responses = responses
114
        self._calls = []
115
116
    def call(self, method, *args):
117
        self._calls.append(('call', method, args))
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
118
        return self.responses.pop(0)[0]
119
120
    def call2(self, method, *args):
121
        self._calls.append(('call2', method, args))
122
        result = self.responses.pop(0)
123
        return result[0], FakeProtocol(result[1])
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
124
125
126
class TestBranchLastRevisionInfo(tests.TestCase):
127
128
    def test_empty_branch(self):
129
        # in an empty branch we decode the response properly
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
130
        client = FakeClient([(('ok', '0', ''), )])
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
131
        transport = MemoryTransport()
132
        transport.mkdir('quack')
133
        transport = transport.clone('quack')
134
        # we do not want bzrdir to make any remote calls
135
        bzrdir = RemoteBzrDir(transport, _client=False)
136
        branch = RemoteBranch(bzrdir, None, _client=client)
137
        result = branch.last_revision_info()
138
139
        self.assertEqual(
140
            [('call', 'Branch.last_revision_info', ('///quack/',))],
141
            client._calls)
142
        self.assertEqual((0, NULL_REVISION), result)
143
144
    def test_non_empty_branch(self):
145
        # in a non-empty branch we also decode the response properly
146
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
147
        client = FakeClient([(('ok', '2', u'\xc8'.encode('utf8')), )])
2018.5.51 by Wouter van Heyst
Test and implement RemoteBranch.last_revision_info()
148
        transport = MemoryTransport()
149
        transport.mkdir('kwaak')
150
        transport = transport.clone('kwaak')
151
        # we do not want bzrdir to make any remote calls
152
        bzrdir = RemoteBzrDir(transport, _client=False)
153
        branch = RemoteBranch(bzrdir, None, _client=client)
154
        result = branch.last_revision_info()
155
156
        self.assertEqual(
157
            [('call', 'Branch.last_revision_info', ('///kwaak/',))],
158
            client._calls)
159
        self.assertEqual((2, u'\xc8'), result)
2018.5.57 by Robert Collins
Implement RemoteRepository.is_shared (Robert Collins, Vincent Ladeuil).
160
161
2018.12.3 by Andrew Bennetts
Add a Branch.set_last_revision smart method, and make RemoteBranch.set_revision_history use it.
162
class TestBranchSetLastRevision(tests.TestCase):
163
164
    def test_set_empty(self):
165
        # set_revision_history([]) is translated to calling
166
        # Branch.set_last_revision(path, '') on the wire.
167
        client = FakeClient([(('ok',), )])
168
        transport = MemoryTransport()
169
        transport.mkdir('branch')
170
        transport = transport.clone('branch')
171
172
        bzrdir = RemoteBzrDir(transport, _client=False)
173
        branch = RemoteBranch(bzrdir, None, _client=client)
174
175
        result = branch.set_revision_history([])
176
        self.assertEqual(
177
            [('call', 'Branch.set_last_revision', ('///branch/', ''))],
178
            client._calls)
179
        self.assertEqual(None, result)
180
181
    def test_set_nonempty(self):
182
        # set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
183
        # Branch.set_last_revision(path, rev-idN) on the wire.
184
        client = FakeClient([(('ok',), )])
185
        transport = MemoryTransport()
186
        transport.mkdir('branch')
187
        transport = transport.clone('branch')
188
189
        bzrdir = RemoteBzrDir(transport, _client=False)
190
        branch = RemoteBranch(bzrdir, None, _client=client)
191
192
        result = branch.set_revision_history(['rev-id1', 'rev-id2'])
193
        self.assertEqual(
194
            [('call', 'Branch.set_last_revision', ('///branch/', 'rev-id2'))],
195
            client._calls)
196
        self.assertEqual(None, result)
197
198
    def test_no_such_revision(self):
199
        # A response of 'NoSuchRevision' is translated into an exception.
200
        client = FakeClient([(('NoSuchRevision', 'rev-id'), )])
201
        transport = MemoryTransport()
202
        transport.mkdir('branch')
203
        transport = transport.clone('branch')
204
205
        bzrdir = RemoteBzrDir(transport, _client=False)
206
        branch = RemoteBranch(bzrdir, None, _client=client)
207
208
        self.assertRaises(
2018.5.78 by Andrew Bennetts
Implement RemoteRepository.lock_write/unlock to expect and send tokens over the
209
            errors.NoSuchRevision, branch.set_revision_history, ['rev-id'])
2018.12.3 by Andrew Bennetts
Add a Branch.set_last_revision smart method, and make RemoteBranch.set_revision_history use it.
210
211
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
212
class TestBranchControlGetBranchConf(tests.TestCase):
213
    """Test branch.control_files api munging...
214
2018.12.3 by Andrew Bennetts
Add a Branch.set_last_revision smart method, and make RemoteBranch.set_revision_history use it.
215
    we special case RemoteBranch.control_files.get('branch.conf') to
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
216
    call a specific API so that RemoteBranch's can intercept configuration
217
    file reading, allowing them to signal to the client about things like
218
    'email is configured for commits'.
219
    """
220
221
    def test_get_branch_conf(self):
222
        # in an empty branch we decode the response properly
223
        client = FakeClient([(('ok', ), 'config file body')])
224
        transport = MemoryTransport()
225
        transport.mkdir('quack')
226
        transport = transport.clone('quack')
227
        # we do not want bzrdir to make any remote calls
228
        bzrdir = RemoteBzrDir(transport, _client=False)
229
        branch = RemoteBranch(bzrdir, None, _client=client)
230
        result = branch.control_files.get('branch.conf')
231
        self.assertEqual(
232
            [('call2', 'Branch.get_config_file', ('///quack/',))],
233
            client._calls)
234
        self.assertEqual('config file body', result.read())
235
236
2018.5.67 by Wouter van Heyst
Implement RemoteRepository.get_revision_graph (Wouter van Heyst, Robert Collins)
237
class TestRemoteRepository(tests.TestCase):
2018.5.57 by Robert Collins
Implement RemoteRepository.is_shared (Robert Collins, Vincent Ladeuil).
238
239
    def setup_fake_client_and_repository(self, responses, transport_path):
240
        """Create the fake client and repository for testing with."""
241
        client = FakeClient(responses)
242
        transport = MemoryTransport()
243
        transport.mkdir(transport_path)
244
        transport = transport.clone(transport_path)
245
        # we do not want bzrdir to make any remote calls
246
        bzrdir = RemoteBzrDir(transport, _client=False)
247
        repo = RemoteRepository(bzrdir, None, _client=client)
248
        return repo, client
249
2018.5.67 by Wouter van Heyst
Implement RemoteRepository.get_revision_graph (Wouter van Heyst, Robert Collins)
250
2018.12.2 by Andrew Bennetts
Remove some duplicate code in test_remote
251
class TestRepositoryGatherStats(TestRemoteRepository):
2018.10.3 by v.ladeuil+lp at free
more tests for gather_stats
252
253
    def test_revid_none(self):
254
        # ('ok',), body with revisions and size
255
        responses = [(('ok', ), 'revisions: 2\nsize: 18\n')]
256
        transport_path = 'quack'
257
        repo, client = self.setup_fake_client_and_repository(
258
            responses, transport_path)
259
        result = repo.gather_stats(None)
260
        self.assertEqual(
261
            [('call2', 'Repository.gather_stats', ('///quack/','','no'))],
262
            client._calls)
263
        self.assertEqual({'revisions': 2, 'size': 18}, result)
264
265
    def test_revid_no_committers(self):
266
        # ('ok',), body without committers
267
        responses = [(('ok', ),
268
                      'firstrev: 123456.300 3600\n'
269
                      'latestrev: 654231.400 0\n'
270
                      'revisions: 2\n'
271
                      'size: 18\n')]
272
        transport_path = 'quick'
273
        revid = u'\xc8'
274
        repo, client = self.setup_fake_client_and_repository(
275
            responses, transport_path)
276
        result = repo.gather_stats(revid)
277
        self.assertEqual(
278
            [('call2', 'Repository.gather_stats',
279
              ('///quick/', revid.encode('utf8'), 'no'))],
280
            client._calls)
281
        self.assertEqual({'revisions': 2, 'size': 18,
282
                          'firstrev': (123456.300, 3600),
283
                          'latestrev': (654231.400, 0),},
284
                         result)
285
286
    def test_revid_with_committers(self):
287
        # ('ok',), body with committers
288
        responses = [(('ok', ),
289
                      'committers: 128\n'
290
                      'firstrev: 123456.300 3600\n'
291
                      'latestrev: 654231.400 0\n'
292
                      'revisions: 2\n'
293
                      'size: 18\n')]
294
        transport_path = 'buick'
295
        revid = u'\xc8'
296
        repo, client = self.setup_fake_client_and_repository(
297
            responses, transport_path)
298
        result = repo.gather_stats(revid, True)
299
        self.assertEqual(
300
            [('call2', 'Repository.gather_stats',
301
              ('///buick/', revid.encode('utf8'), 'yes'))],
302
            client._calls)
303
        self.assertEqual({'revisions': 2, 'size': 18,
304
                          'committers': 128,
305
                          'firstrev': (123456.300, 3600),
306
                          'latestrev': (654231.400, 0),},
307
                         result)
308
309
2018.5.68 by Wouter van Heyst
Merge RemoteRepository.gather_stats.
310
class TestRepositoryGetRevisionGraph(TestRemoteRepository):
311
    
312
    def test_null_revision(self):
313
        # a null revision has the predictable result {}, we should have no wire
314
        # traffic when calling it with this argument
315
        responses = [(('notused', ), '')]
316
        transport_path = 'empty'
317
        repo, client = self.setup_fake_client_and_repository(
318
            responses, transport_path)
319
        result = repo.get_revision_graph(NULL_REVISION)
320
        self.assertEqual([], client._calls)
321
        self.assertEqual({}, result)
322
2018.5.67 by Wouter van Heyst
Implement RemoteRepository.get_revision_graph (Wouter van Heyst, Robert Collins)
323
    def test_none_revision(self):
324
        # with none we want the entire graph
325
        r1 = u'\u0e33'
326
        r2 = u'\u0dab'
327
        lines = [' '.join([r2, r1]), r1]
328
        encoded_body = '\n'.join(lines).encode('utf8')
329
330
        responses = [(('ok', ), encoded_body)]
331
        transport_path = 'sinhala'
332
        repo, client = self.setup_fake_client_and_repository(
333
            responses, transport_path)
334
        result = repo.get_revision_graph()
335
        self.assertEqual(
336
            [('call2', 'Repository.get_revision_graph', ('///sinhala/', ''))],
337
            client._calls)
338
        self.assertEqual({r1: [], r2: [r1]}, result)
339
340
    def test_specific_revision(self):
341
        # with a specific revision we want the graph for that
342
        # with none we want the entire graph
343
        r11 = u'\u0e33'
344
        r12 = u'\xc9'
345
        r2 = u'\u0dab'
346
        lines = [' '.join([r2, r11, r12]), r11, r12]
347
        encoded_body = '\n'.join(lines).encode('utf8')
348
349
        responses = [(('ok', ), encoded_body)]
350
        transport_path = 'sinhala'
351
        repo, client = self.setup_fake_client_and_repository(
352
            responses, transport_path)
353
        result = repo.get_revision_graph(r2)
354
        self.assertEqual(
355
            [('call2', 'Repository.get_revision_graph', ('///sinhala/', r2.encode('utf8')))],
356
            client._calls)
357
        self.assertEqual({r11: [], r12: [], r2: [r11, r12], }, result)
358
359
    def test_no_such_revision(self):
360
        revid = '123'
361
        responses = [(('nosuchrevision', revid), '')]
362
        transport_path = 'sinhala'
363
        repo, client = self.setup_fake_client_and_repository(
364
            responses, transport_path)
365
        # also check that the right revision is reported in the error
2018.5.78 by Andrew Bennetts
Implement RemoteRepository.lock_write/unlock to expect and send tokens over the
366
        self.assertRaises(errors.NoSuchRevision,
2018.5.67 by Wouter van Heyst
Implement RemoteRepository.get_revision_graph (Wouter van Heyst, Robert Collins)
367
            repo.get_revision_graph, revid)
368
        self.assertEqual(
369
            [('call2', 'Repository.get_revision_graph', ('///sinhala/', revid))],
370
            client._calls)
371
372
        
373
class TestRepositoryIsShared(TestRemoteRepository):
374
2018.5.57 by Robert Collins
Implement RemoteRepository.is_shared (Robert Collins, Vincent Ladeuil).
375
    def test_is_shared(self):
376
        # ('yes', ) for Repository.is_shared -> 'True'.
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
377
        responses = [(('yes', ), )]
2018.5.57 by Robert Collins
Implement RemoteRepository.is_shared (Robert Collins, Vincent Ladeuil).
378
        transport_path = 'quack'
379
        repo, client = self.setup_fake_client_and_repository(
380
            responses, transport_path)
381
        result = repo.is_shared()
382
        self.assertEqual(
383
            [('call', 'Repository.is_shared', ('///quack/',))],
384
            client._calls)
385
        self.assertEqual(True, result)
386
387
    def test_is_not_shared(self):
388
        # ('no', ) for Repository.is_shared -> 'False'.
2018.5.59 by Robert Collins
Get BranchConfig working somewhat on RemoteBranches (Robert Collins, Vincent Ladeuil).
389
        responses = [(('no', ), )]
2018.5.57 by Robert Collins
Implement RemoteRepository.is_shared (Robert Collins, Vincent Ladeuil).
390
        transport_path = 'qwack'
391
        repo, client = self.setup_fake_client_and_repository(
392
            responses, transport_path)
393
        result = repo.is_shared()
394
        self.assertEqual(
395
            [('call', 'Repository.is_shared', ('///qwack/',))],
396
            client._calls)
397
        self.assertEqual(False, result)
2018.5.78 by Andrew Bennetts
Implement RemoteRepository.lock_write/unlock to expect and send tokens over the
398
399
400
class TestRepositoryLockWrite(TestRemoteRepository):
401
402
    def test_lock_write(self):
403
        responses = [(('ok', 'a token'), '')]
404
        transport_path = 'quack'
405
        repo, client = self.setup_fake_client_and_repository(
406
            responses, transport_path)
407
        result = repo.lock_write()
408
        self.assertEqual(
409
            [('call', 'Repository.lock_write', ('///quack/',))],
410
            client._calls)
411
        self.assertEqual('a token', result)
412
413
    def test_lock_write_already_locked(self):
414
        responses = [(('LockContention', ), '')]
415
        transport_path = 'quack'
416
        repo, client = self.setup_fake_client_and_repository(
417
            responses, transport_path)
418
        self.assertRaises(errors.LockContention, repo.lock_write)
419
        self.assertEqual(
420
            [('call', 'Repository.lock_write', ('///quack/',))],
421
            client._calls)
422
423
424
class TestRepositoryUnlock(TestRemoteRepository):
425
426
    def test_unlock(self):
427
        responses = [(('ok', 'a token'), ''),
428
                     (('ok',), '')]
429
        transport_path = 'quack'
430
        repo, client = self.setup_fake_client_and_repository(
431
            responses, transport_path)
432
        repo.lock_write()
433
        repo.unlock()
434
        self.assertEqual(
435
            [('call', 'Repository.lock_write', ('///quack/',)),
436
             ('call', 'Repository.unlock', ('///quack/', 'a token'))],
437
            client._calls)
438
439
    def test_unlock_wrong_token(self):
440
        # If somehow the token is wrong, unlock will raise TokenMismatch.
441
        responses = [(('ok', 'a token'), ''),
442
                     (('TokenMismatch',), '')]
443
        transport_path = 'quack'
444
        repo, client = self.setup_fake_client_and_repository(
445
            responses, transport_path)
446
        repo.lock_write()
447
        self.assertRaises(errors.TokenMismatch, repo.unlock)
448
449