~bzr-pqm/bzr/bzr.dev

907.1.48 by John Arbash Meinel
Updated LocalTransport by passing it through the transport_test suite, and got it to pass.
1
# Copyright (C) 2004, 2005 by Canonical Ltd
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
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
18
import os
19
from cStringIO import StringIO
1442.1.44 by Robert Collins
Many transport related tweaks:
20
1185.35.31 by Aaron Bentley
Throw ConnectionError instead of NoSuchFile except when we get a 404
21
from bzrlib.errors import (NoSuchFile, FileExists, TransportNotPossible,
22
                           ConnectionError)
1442.1.44 by Robert Collins
Many transport related tweaks:
23
from bzrlib.selftest import TestCase, TestCaseInTempDir
1185.11.10 by John Arbash Meinel
Fixing up the test suite.
24
from bzrlib.selftest.HTTPTestUtil import TestCaseWithWebserver
1469 by Robert Collins
Change Transport.* to work with URL's.
25
from bzrlib.transport import memory, urlescape
1442.1.44 by Robert Collins
Many transport related tweaks:
26
1185.11.19 by John Arbash Meinel
Testing put and append, also testing agaist file-like objects as well as strings.
27
28
def _append(fn, txt):
29
    """Append the given text (file-like object) to the supplied filename."""
30
    f = open(fn, 'ab')
31
    f.write(txt)
32
    f.flush()
33
    f.close()
34
    del f
1185.11.1 by John Arbash Meinel
(broken) Transport work is merged in. Tests do not pass yet.
35
1469 by Robert Collins
Change Transport.* to work with URL's.
36
class TestTransport(TestCase):
37
    """Test the non transport-concrete class functionality."""
38
39
    def test_urlescape(self):
40
        self.assertEqual('%25', urlescape('%'))
41
42
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
43
class TestTransportMixIn(object):
44
    """Subclass this, and it will provide a series of tests for a Transport.
45
    It assumes that the Transport object is connected to the 
46
    current working directory.  So that whatever is done 
47
    through the transport, should show up in the working 
48
    directory, and vice-versa.
1185.11.1 by John Arbash Meinel
(broken) Transport work is merged in. Tests do not pass yet.
49
50
    This also tests to make sure that the functions work with both
51
    generators and lists (assuming iter(list) is effectively a generator)
52
    """
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
53
    readonly = False
54
    def get_transport(self):
55
        """Children should override this to return the Transport object.
56
        """
57
        raise NotImplementedError
58
59
    def test_has(self):
60
        t = self.get_transport()
61
1469 by Robert Collins
Change Transport.* to work with URL's.
62
        files = ['a', 'b', 'e', 'g', '%']
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
63
        self.build_tree(files)
64
        self.assertEqual(t.has('a'), True)
65
        self.assertEqual(t.has('c'), False)
1469 by Robert Collins
Change Transport.* to work with URL's.
66
        self.assertEqual(t.has(urlescape('%')), True)
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
67
        self.assertEqual(list(t.has_multi(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])),
68
                [True, True, False, False, True, False, True, False])
69
        self.assertEqual(list(t.has_multi(iter(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']))),
70
                [True, True, False, False, True, False, True, False])
71
72
    def test_get(self):
73
        t = self.get_transport()
74
75
        files = ['a', 'b', 'e', 'g']
76
        self.build_tree(files)
77
        self.assertEqual(t.get('a').read(), open('a').read())
78
        content_f = t.get_multi(files)
79
        for path,f in zip(files, content_f):
80
            self.assertEqual(open(path).read(), f.read())
81
82
        content_f = t.get_multi(iter(files))
83
        for path,f in zip(files, content_f):
84
            self.assertEqual(open(path).read(), f.read())
85
86
        self.assertRaises(NoSuchFile, t.get, 'c')
87
        try:
88
            files = list(t.get_multi(['a', 'b', 'c']))
89
        except NoSuchFile:
90
            pass
91
        else:
92
            self.fail('Failed to raise NoSuchFile for missing file in get_multi')
93
        try:
94
            files = list(t.get_multi(iter(['a', 'b', 'c', 'e'])))
95
        except NoSuchFile:
96
            pass
97
        else:
98
            self.fail('Failed to raise NoSuchFile for missing file in get_multi')
99
100
    def test_put(self):
101
        t = self.get_transport()
102
103
        if self.readonly:
104
            self.assertRaises(TransportNotPossible,
105
                    t.put, 'a', 'some text for a\n')
106
            open('a', 'wb').write('some text for a\n')
107
        else:
108
            t.put('a', 'some text for a\n')
109
        self.assert_(os.path.exists('a'))
110
        self.check_file_contents('a', 'some text for a\n')
111
        self.assertEqual(t.get('a').read(), 'some text for a\n')
112
        # Make sure 'has' is updated
113
        self.assertEqual(list(t.has_multi(['a', 'b', 'c', 'd', 'e'])),
114
                [True, False, False, False, False])
115
        if self.readonly:
116
            self.assertRaises(TransportNotPossible,
117
                    t.put_multi,
118
                    [('a', 'new\ncontents for\na\n'),
119
                        ('d', 'contents\nfor d\n')])
120
            open('a', 'wb').write('new\ncontents for\na\n')
121
            open('d', 'wb').write('contents\nfor d\n')
122
        else:
123
            # Put also replaces contents
124
            self.assertEqual(t.put_multi([('a', 'new\ncontents for\na\n'),
125
                                          ('d', 'contents\nfor d\n')]),
126
                             2)
127
        self.assertEqual(list(t.has_multi(['a', 'b', 'c', 'd', 'e'])),
128
                [True, False, False, True, False])
129
        self.check_file_contents('a', 'new\ncontents for\na\n')
130
        self.check_file_contents('d', 'contents\nfor d\n')
131
132
        if self.readonly:
133
            self.assertRaises(TransportNotPossible,
134
                t.put_multi, iter([('a', 'diff\ncontents for\na\n'),
135
                                  ('d', 'another contents\nfor d\n')]))
136
            open('a', 'wb').write('diff\ncontents for\na\n')
137
            open('d', 'wb').write('another contents\nfor d\n')
138
        else:
139
            self.assertEqual(
140
                t.put_multi(iter([('a', 'diff\ncontents for\na\n'),
141
                                  ('d', 'another contents\nfor d\n')]))
142
                             , 2)
143
        self.check_file_contents('a', 'diff\ncontents for\na\n')
144
        self.check_file_contents('d', 'another contents\nfor d\n')
145
146
        if self.readonly:
147
            self.assertRaises(TransportNotPossible,
148
                    t.put, 'path/doesnt/exist/c', 'contents')
149
        else:
150
            self.assertRaises(NoSuchFile,
151
                    t.put, 'path/doesnt/exist/c', 'contents')
152
153
    def test_put_file(self):
154
        t = self.get_transport()
155
156
        # Test that StringIO can be used as a file-like object with put
157
        f1 = StringIO('this is a string\nand some more stuff\n')
158
        if self.readonly:
159
            open('f1', 'wb').write(f1.read())
160
        else:
161
            t.put('f1', f1)
162
163
        del f1
164
165
        self.check_file_contents('f1', 
166
                'this is a string\nand some more stuff\n')
167
168
        f2 = StringIO('here is some text\nand a bit more\n')
169
        f3 = StringIO('some text for the\nthird file created\n')
170
171
        if self.readonly:
172
            open('f2', 'wb').write(f2.read())
173
            open('f3', 'wb').write(f3.read())
174
        else:
175
            t.put_multi([('f2', f2), ('f3', f3)])
176
177
        del f2, f3
178
179
        self.check_file_contents('f2', 'here is some text\nand a bit more\n')
180
        self.check_file_contents('f3', 'some text for the\nthird file created\n')
181
182
        # Test that an actual file object can be used with put
183
        f4 = open('f1', 'rb')
184
        if self.readonly:
185
            open('f4', 'wb').write(f4.read())
186
        else:
187
            t.put('f4', f4)
188
189
        del f4
190
191
        self.check_file_contents('f4', 
192
                'this is a string\nand some more stuff\n')
193
194
        f5 = open('f2', 'rb')
195
        f6 = open('f3', 'rb')
196
        if self.readonly:
197
            open('f5', 'wb').write(f5.read())
198
            open('f6', 'wb').write(f6.read())
199
        else:
200
            t.put_multi([('f5', f5), ('f6', f6)])
201
202
        del f5, f6
203
204
        self.check_file_contents('f5', 'here is some text\nand a bit more\n')
205
        self.check_file_contents('f6', 'some text for the\nthird file created\n')
206
207
208
209
    def test_mkdir(self):
210
        t = self.get_transport()
211
212
        # Test mkdir
213
        os.mkdir('dir_a')
214
        self.assertEqual(t.has('dir_a'), True)
215
        self.assertEqual(t.has('dir_b'), False)
216
217
        if self.readonly:
218
            self.assertRaises(TransportNotPossible,
219
                    t.mkdir, 'dir_b')
220
            os.mkdir('dir_b')
221
        else:
222
            t.mkdir('dir_b')
223
        self.assertEqual(t.has('dir_b'), True)
224
        self.assert_(os.path.isdir('dir_b'))
225
226
        if self.readonly:
227
            self.assertRaises(TransportNotPossible,
228
                    t.mkdir_multi, ['dir_c', 'dir_d'])
229
            os.mkdir('dir_c')
230
            os.mkdir('dir_d')
231
        else:
232
            t.mkdir_multi(['dir_c', 'dir_d'])
233
234
        if self.readonly:
235
            self.assertRaises(TransportNotPossible,
236
                    t.mkdir_multi, iter(['dir_e', 'dir_f']))
237
            os.mkdir('dir_e')
238
            os.mkdir('dir_f')
239
        else:
240
            t.mkdir_multi(iter(['dir_e', 'dir_f']))
241
        self.assertEqual(list(t.has_multi(
242
            ['dir_a', 'dir_b', 'dir_c', 'dir_q',
243
             'dir_d', 'dir_e', 'dir_f', 'dir_b'])),
244
            [True, True, True, False,
245
             True, True, True, True])
246
        for d in ['dir_a', 'dir_b', 'dir_c', 'dir_d', 'dir_e', 'dir_f']:
247
            self.assert_(os.path.isdir(d))
248
249
        if not self.readonly:
250
            self.assertRaises(NoSuchFile, t.mkdir, 'path/doesnt/exist')
251
            self.assertRaises(FileExists, t.mkdir, 'dir_a') # Creating a directory again should fail
252
253
        # Make sure the transport recognizes when a
254
        # directory is created by other means
255
        # Caching Transports will fail, because dir_e was already seen not
256
        # to exist. So instead, we will search for a new directory
257
        #os.mkdir('dir_e')
258
        #if not self.readonly:
259
        #    self.assertRaises(FileExists, t.mkdir, 'dir_e')
260
261
        os.mkdir('dir_g')
262
        if not self.readonly:
263
            self.assertRaises(FileExists, t.mkdir, 'dir_g')
264
265
        # Test get/put in sub-directories
266
        if self.readonly:
267
            open('dir_a/a', 'wb').write('contents of dir_a/a')
268
            open('dir_b/b', 'wb').write('contents of dir_b/b')
269
        else:
270
            self.assertEqual(
271
                t.put_multi([('dir_a/a', 'contents of dir_a/a'),
272
                             ('dir_b/b', 'contents of dir_b/b')])
273
                          , 2)
274
        for f in ('dir_a/a', 'dir_b/b'):
275
            self.assertEqual(t.get(f).read(), open(f).read())
276
277
    def test_copy_to(self):
278
        import tempfile
279
        from bzrlib.transport.local import LocalTransport
280
281
        t = self.get_transport()
282
283
        files = ['a', 'b', 'c', 'd']
284
        self.build_tree(files)
285
286
        dtmp = tempfile.mkdtemp(dir='.', prefix='test-transport-')
287
        dtmp_base = os.path.basename(dtmp)
288
        local_t = LocalTransport(dtmp)
289
290
        t.copy_to(files, local_t)
291
        for f in files:
292
            self.assertEquals(open(f).read(),
293
                    open(os.path.join(dtmp_base, f)).read())
294
295
        del dtmp, dtmp_base, local_t
296
297
        dtmp = tempfile.mkdtemp(dir='.', prefix='test-transport-')
298
        dtmp_base = os.path.basename(dtmp)
299
        local_t = LocalTransport(dtmp)
300
301
        files = ['a', 'b', 'c', 'd']
302
        t.copy_to(iter(files), local_t)
303
        for f in files:
304
            self.assertEquals(open(f).read(),
305
                    open(os.path.join(dtmp_base, f)).read())
306
307
        del dtmp, dtmp_base, local_t
308
309
    def test_append(self):
310
        t = self.get_transport()
311
312
        if self.readonly:
313
            open('a', 'wb').write('diff\ncontents for\na\n')
314
            open('b', 'wb').write('contents\nfor b\n')
315
        else:
316
            t.put_multi([
317
                    ('a', 'diff\ncontents for\na\n'),
318
                    ('b', 'contents\nfor b\n')
319
                    ])
320
321
        if self.readonly:
322
            self.assertRaises(TransportNotPossible,
323
                    t.append, 'a', 'add\nsome\nmore\ncontents\n')
324
            _append('a', 'add\nsome\nmore\ncontents\n')
325
        else:
326
            t.append('a', 'add\nsome\nmore\ncontents\n')
327
328
        self.check_file_contents('a', 
329
            'diff\ncontents for\na\nadd\nsome\nmore\ncontents\n')
330
331
        if self.readonly:
332
            self.assertRaises(TransportNotPossible,
333
                    t.append_multi,
334
                        [('a', 'and\nthen\nsome\nmore\n'),
335
                         ('b', 'some\nmore\nfor\nb\n')])
336
            _append('a', 'and\nthen\nsome\nmore\n')
337
            _append('b', 'some\nmore\nfor\nb\n')
338
        else:
339
            t.append_multi([('a', 'and\nthen\nsome\nmore\n'),
340
                    ('b', 'some\nmore\nfor\nb\n')])
341
        self.check_file_contents('a', 
342
            'diff\ncontents for\na\n'
343
            'add\nsome\nmore\ncontents\n'
344
            'and\nthen\nsome\nmore\n')
345
        self.check_file_contents('b', 
346
                'contents\nfor b\n'
347
                'some\nmore\nfor\nb\n')
348
349
        if self.readonly:
350
            _append('a', 'a little bit more\n')
351
            _append('b', 'from an iterator\n')
352
        else:
353
            t.append_multi(iter([('a', 'a little bit more\n'),
354
                    ('b', 'from an iterator\n')]))
355
        self.check_file_contents('a', 
356
            'diff\ncontents for\na\n'
357
            'add\nsome\nmore\ncontents\n'
358
            'and\nthen\nsome\nmore\n'
359
            'a little bit more\n')
360
        self.check_file_contents('b', 
361
                'contents\nfor b\n'
362
                'some\nmore\nfor\nb\n'
363
                'from an iterator\n')
364
365
    def test_append_file(self):
366
        t = self.get_transport()
367
368
        contents = [
369
            ('f1', 'this is a string\nand some more stuff\n'),
370
            ('f2', 'here is some text\nand a bit more\n'),
371
            ('f3', 'some text for the\nthird file created\n'),
372
            ('f4', 'this is a string\nand some more stuff\n'),
373
            ('f5', 'here is some text\nand a bit more\n'),
374
            ('f6', 'some text for the\nthird file created\n')
375
        ]
376
        
377
        if self.readonly:
378
            for f, val in contents:
379
                open(f, 'wb').write(val)
380
        else:
381
            t.put_multi(contents)
382
383
        a1 = StringIO('appending to\none\n')
384
        if self.readonly:
385
            _append('f1', a1.read())
386
        else:
387
            t.append('f1', a1)
388
389
        del a1
390
391
        self.check_file_contents('f1', 
392
                'this is a string\nand some more stuff\n'
393
                'appending to\none\n')
394
395
        a2 = StringIO('adding more\ntext to two\n')
396
        a3 = StringIO('some garbage\nto put in three\n')
397
398
        if self.readonly:
399
            _append('f2', a2.read())
400
            _append('f3', a3.read())
401
        else:
402
            t.append_multi([('f2', a2), ('f3', a3)])
403
404
        del a2, a3
405
406
        self.check_file_contents('f2',
407
                'here is some text\nand a bit more\n'
408
                'adding more\ntext to two\n')
409
        self.check_file_contents('f3', 
410
                'some text for the\nthird file created\n'
411
                'some garbage\nto put in three\n')
412
413
        # Test that an actual file object can be used with put
414
        a4 = open('f1', 'rb')
415
        if self.readonly:
416
            _append('f4', a4.read())
417
        else:
418
            t.append('f4', a4)
419
420
        del a4
421
422
        self.check_file_contents('f4', 
423
                'this is a string\nand some more stuff\n'
424
                'this is a string\nand some more stuff\n'
425
                'appending to\none\n')
426
427
        a5 = open('f2', 'rb')
428
        a6 = open('f3', 'rb')
429
        if self.readonly:
430
            _append('f5', a5.read())
431
            _append('f6', a6.read())
432
        else:
433
            t.append_multi([('f5', a5), ('f6', a6)])
434
435
        del a5, a6
436
437
        self.check_file_contents('f5',
438
                'here is some text\nand a bit more\n'
439
                'here is some text\nand a bit more\n'
440
                'adding more\ntext to two\n')
441
        self.check_file_contents('f6',
442
                'some text for the\nthird file created\n'
443
                'some text for the\nthird file created\n'
444
                'some garbage\nto put in three\n')
445
446
    def test_delete(self):
447
        # TODO: Test Transport.delete
448
        pass
449
450
    def test_move(self):
451
        # TODO: Test Transport.move
452
        pass
453
1185.35.31 by Aaron Bentley
Throw ConnectionError instead of NoSuchFile except when we get a 404
454
    def test_connection_error(self):
455
        """ConnectionError is raised when connection is impossible"""
456
        if not hasattr(self, "get_bogus_transport"):
457
            return
458
        t = self.get_bogus_transport()
459
        self.assertRaises(ConnectionError, t.get, '.bzr/branch')
1442.1.44 by Robert Collins
Many transport related tweaks:
460
1185.35.31 by Aaron Bentley
Throw ConnectionError instead of NoSuchFile except when we get a 404
461
        
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
462
class LocalTransportTest(TestCaseInTempDir, TestTransportMixIn):
463
    def get_transport(self):
464
        from bzrlib.transport.local import LocalTransport
465
        return LocalTransport('.')
466
1442.1.44 by Robert Collins
Many transport related tweaks:
467
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
468
class HttpTransportTest(TestCaseWithWebserver, TestTransportMixIn):
1442.1.44 by Robert Collins
Many transport related tweaks:
469
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
470
    readonly = True
1442.1.44 by Robert Collins
Many transport related tweaks:
471
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
472
    def get_transport(self):
1185.11.1 by John Arbash Meinel
(broken) Transport work is merged in. Tests do not pass yet.
473
        from bzrlib.transport.http import HttpTransport
1185.11.15 by John Arbash Meinel
Got HttpTransport tests to pass. Check for EAGAIN, pass permit_failure around, etc
474
        url = self.get_remote_url('.')
1185.11.22 by John Arbash Meinel
Major refactoring of testtransport.
475
        return HttpTransport(url)
907.1.50 by John Arbash Meinel
Removed encode/decode from Transport.put/get, added more exceptions that can be thrown.
476
1185.35.31 by Aaron Bentley
Throw ConnectionError instead of NoSuchFile except when we get a 404
477
    def get_bogus_transport(self):
478
        from bzrlib.transport.http import HttpTransport
1185.33.17 by Martin Pool
[merge] aaron, various fixes
479
        return HttpTransport('http://jasldkjsalkdjalksjdkljasd')
1185.35.31 by Aaron Bentley
Throw ConnectionError instead of NoSuchFile except when we get a 404
480
1442.1.44 by Robert Collins
Many transport related tweaks:
481
482
class TestMemoryTransport(TestCase):
483
484
    def test_get_transport(self):
485
        memory.MemoryTransport()
486
487
    def test_clone(self):
488
        transport = memory.MemoryTransport()
489
        self.failUnless(transport.clone() is transport)
490
491
    def test_abspath(self):
492
        transport = memory.MemoryTransport()
493
        self.assertEqual("in-memory:relpath", transport.abspath('relpath'))
494
495
    def test_relpath(self):
496
        transport = memory.MemoryTransport()
497
498
    def test_append_and_get(self):
499
        transport = memory.MemoryTransport()
500
        transport.append('path', StringIO('content'))
501
        self.assertEqual(transport.get('path').read(), 'content')
502
        transport.append('path', StringIO('content'))
503
        self.assertEqual(transport.get('path').read(), 'contentcontent')
504
505
    def test_put_and_get(self):
506
        transport = memory.MemoryTransport()
507
        transport.put('path', StringIO('content'))
508
        self.assertEqual(transport.get('path').read(), 'content')
509
        transport.put('path', StringIO('content'))
510
        self.assertEqual(transport.get('path').read(), 'content')
511
512
    def test_append_without_dir_fails(self):
513
        transport = memory.MemoryTransport()
514
        self.assertRaises(NoSuchFile,
515
                          transport.append, 'dir/path', StringIO('content'))
516
517
    def test_put_without_dir_fails(self):
518
        transport = memory.MemoryTransport()
519
        self.assertRaises(NoSuchFile,
520
                          transport.put, 'dir/path', StringIO('content'))
521
522
    def test_get_missing(self):
523
        transport = memory.MemoryTransport()
524
        self.assertRaises(NoSuchFile, transport.get, 'foo')
525
526
    def test_has_missing(self):
527
        transport = memory.MemoryTransport()
528
        self.assertEquals(False, transport.has('foo'))
529
530
    def test_has_present(self):
531
        transport = memory.MemoryTransport()
532
        transport.append('foo', StringIO('content'))
533
        self.assertEquals(True, transport.has('foo'))
534
535
    def test_mkdir(self):
536
        transport = memory.MemoryTransport()
537
        transport.mkdir('dir')
538
        transport.append('dir/path', StringIO('content'))
539
        self.assertEqual(transport.get('dir/path').read(), 'content')
540
541
    def test_mkdir_missing_parent(self):
542
        transport = memory.MemoryTransport()
543
        self.assertRaises(NoSuchFile,
544
                          transport.mkdir, 'dir/dir')
545
546
    def test_mkdir_twice(self):
547
        transport = memory.MemoryTransport()
548
        transport.mkdir('dir')
549
        self.assertRaises(FileExists, transport.mkdir, 'dir')
550
551
    def test_parameters(self):
552
        transport = memory.MemoryTransport()
553
        self.assertEqual(True, transport.listable())
554
        self.assertEqual(False, transport.should_cache())
555
556
    def test_iter_files_recursive(self):
557
        transport = memory.MemoryTransport()
558
        transport.mkdir('dir')
559
        transport.put('dir/foo', StringIO('content'))
560
        transport.put('dir/bar', StringIO('content'))
561
        transport.put('bar', StringIO('content'))
562
        paths = set(transport.iter_files_recursive())
563
        self.assertEqual(set(['dir/foo', 'dir/bar', 'bar']), paths)
564
565
    def test_stat(self):
566
        transport = memory.MemoryTransport()
567
        transport.put('foo', StringIO('content'))
568
        transport.put('bar', StringIO('phowar'))
569
        self.assertEqual(7, transport.stat('foo').st_size)
570
        self.assertEqual(6, transport.stat('bar').st_size)
1185.35.31 by Aaron Bentley
Throw ConnectionError instead of NoSuchFile except when we get a 404
571