~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_errors.py

  • Committer: John Arbash Meinel
  • Date: 2008-05-28 23:20:33 UTC
  • mto: This revision was merged to the branch mainline in revision 3458.
  • Revision ID: john@arbash-meinel.com-20080528232033-cx3l3yg845udklps
Bring back always in the form of 'override'.
Change the functions so that they are compatible with the released
definition, and just allow callers to supply override=False
if they want the new behavior.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006 by Canonical Ltd
 
1
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
2
2
#   Authors: Robert Collins <robert.collins@canonical.com>
 
3
#            and others
3
4
#
4
5
# This program is free software; you can redistribute it and/or modify
5
6
# it under the terms of the GNU General Public License as published by
17
18
 
18
19
"""Tests for the formatting and construction of errors."""
19
20
 
20
 
import bzrlib.bzrdir as bzrdir
21
 
import bzrlib.errors as errors
22
 
from bzrlib.tests import TestCaseWithTransport
 
21
from bzrlib import (
 
22
    bzrdir,
 
23
    errors,
 
24
    osutils,
 
25
    symbol_versioning,
 
26
    urlutils,
 
27
    )
 
28
from bzrlib.tests import TestCase, TestCaseWithTransport
23
29
 
24
30
 
25
31
class TestErrors(TestCaseWithTransport):
26
32
 
 
33
    def test_corrupt_dirstate(self):
 
34
        error = errors.CorruptDirstate('path/to/dirstate', 'the reason why')
 
35
        self.assertEqualDiff(
 
36
            "Inconsistency in dirstate file path/to/dirstate.\n"
 
37
            "Error: the reason why",
 
38
            str(error))
 
39
 
 
40
    def test_disabled_method(self):
 
41
        error = errors.DisabledMethod("class name")
 
42
        self.assertEqualDiff(
 
43
            "The smart server method 'class name' is disabled.", str(error))
 
44
 
 
45
    def test_duplicate_file_id(self):
 
46
        error = errors.DuplicateFileId('a_file_id', 'foo')
 
47
        self.assertEqualDiff('File id {a_file_id} already exists in inventory'
 
48
                             ' as foo', str(error))
 
49
 
 
50
    def test_duplicate_help_prefix(self):
 
51
        error = errors.DuplicateHelpPrefix('foo')
 
52
        self.assertEqualDiff('The prefix foo is in the help search path twice.',
 
53
            str(error))
 
54
 
 
55
    def test_incompatibleAPI(self):
 
56
        error = errors.IncompatibleAPI("module", (1, 2, 3), (4, 5, 6), (7, 8, 9))
 
57
        self.assertEqualDiff(
 
58
            'The API for "module" is not compatible with "(1, 2, 3)". '
 
59
            'It supports versions "(4, 5, 6)" to "(7, 8, 9)".',
 
60
            str(error))
 
61
 
 
62
    def test_inconsistent_delta(self):
 
63
        error = errors.InconsistentDelta('path', 'file-id', 'reason for foo')
 
64
        self.assertEqualDiff(
 
65
            "An inconsistent delta was supplied involving 'path', 'file-id'\n"
 
66
            "reason: reason for foo",
 
67
            str(error))
 
68
 
 
69
    def test_in_process_transport(self):
 
70
        error = errors.InProcessTransport('fpp')
 
71
        self.assertEqualDiff(
 
72
            "The transport 'fpp' is only accessible within this process.",
 
73
            str(error))
 
74
 
 
75
    def test_invalid_http_range(self):
 
76
        error = errors.InvalidHttpRange('path',
 
77
                                        'Content-Range: potatoes 0-00/o0oo0',
 
78
                                        'bad range')
 
79
        self.assertEquals("Invalid http range"
 
80
                          " 'Content-Range: potatoes 0-00/o0oo0'"
 
81
                          " for path: bad range",
 
82
                          str(error))
 
83
 
 
84
    def test_invalid_range(self):
 
85
        error = errors.InvalidRange('path', 12, 'bad range')
 
86
        self.assertEquals("Invalid range access in path at 12: bad range",
 
87
                          str(error))
 
88
 
 
89
    def test_inventory_modified(self):
 
90
        error = errors.InventoryModified("a tree to be repred")
 
91
        self.assertEqualDiff("The current inventory for the tree 'a tree to "
 
92
            "be repred' has been modified, so a clean inventory cannot be "
 
93
            "read without data loss.",
 
94
            str(error))
 
95
 
 
96
    def test_install_failed(self):
 
97
        error = errors.InstallFailed(['rev-one'])
 
98
        self.assertEqual("Could not install revisions:\nrev-one", str(error))
 
99
        error = errors.InstallFailed(['rev-one', 'rev-two'])
 
100
        self.assertEqual("Could not install revisions:\nrev-one, rev-two",
 
101
                         str(error))
 
102
        error = errors.InstallFailed([None])
 
103
        self.assertEqual("Could not install revisions:\nNone", str(error))
 
104
 
 
105
    def test_lock_active(self):
 
106
        error = errors.LockActive("lock description")
 
107
        self.assertEqualDiff("The lock for 'lock description' is in use and "
 
108
            "cannot be broken.",
 
109
            str(error))
 
110
 
 
111
    def test_knit_data_stream_incompatible(self):
 
112
        error = errors.KnitDataStreamIncompatible(
 
113
            'stream format', 'target format')
 
114
        self.assertEqual('Cannot insert knit data stream of format '
 
115
                         '"stream format" into knit of format '
 
116
                         '"target format".', str(error))
 
117
 
 
118
    def test_knit_data_stream_unknown(self):
 
119
        error = errors.KnitDataStreamUnknown(
 
120
            'stream format')
 
121
        self.assertEqual('Cannot parse knit data stream of format '
 
122
                         '"stream format".', str(error))
 
123
 
 
124
    def test_knit_header_error(self):
 
125
        error = errors.KnitHeaderError('line foo\n', 'path/to/file')
 
126
        self.assertEqual("Knit header error: 'line foo\\n' unexpected"
 
127
                         " for file \"path/to/file\".", str(error))
 
128
 
 
129
    def test_knit_index_unknown_method(self):
 
130
        error = errors.KnitIndexUnknownMethod('http://host/foo.kndx',
 
131
                                              ['bad', 'no-eol'])
 
132
        self.assertEqual("Knit index http://host/foo.kndx does not have a"
 
133
                         " known method in options: ['bad', 'no-eol']",
 
134
                         str(error))
 
135
 
 
136
    def test_medium_not_connected(self):
 
137
        error = errors.MediumNotConnected("a medium")
 
138
        self.assertEqualDiff(
 
139
            "The medium 'a medium' is not connected.", str(error))
 
140
 
 
141
    def test_no_public_branch(self):
 
142
        b = self.make_branch('.')
 
143
        error = errors.NoPublicBranch(b)
 
144
        url = urlutils.unescape_for_display(b.base, 'ascii')
 
145
        self.assertEqualDiff(
 
146
            'There is no public branch set for "%s".' % url, str(error))
 
147
 
27
148
    def test_no_repo(self):
28
149
        dir = bzrdir.BzrDir.create(self.get_url())
29
150
        error = errors.NoRepositoryPresent(dir)
30
 
        self.assertNotEqual(-1, str(error).find(repr(dir.transport.clone('..').base)))
31
 
        self.assertEqual(-1, str(error).find(repr(dir.transport.base)))
 
151
        self.assertNotEqual(-1, str(error).find((dir.transport.clone('..').base)))
 
152
        self.assertEqual(-1, str(error).find((dir.transport.base)))
 
153
        
 
154
    def test_no_smart_medium(self):
 
155
        error = errors.NoSmartMedium("a transport")
 
156
        self.assertEqualDiff("The transport 'a transport' cannot tunnel the "
 
157
            "smart protocol.",
 
158
            str(error))
 
159
 
 
160
    def test_no_help_topic(self):
 
161
        error = errors.NoHelpTopic("topic")
 
162
        self.assertEqualDiff("No help could be found for 'topic'. "
 
163
            "Please use 'bzr help topics' to obtain a list of topics.",
 
164
            str(error))
 
165
 
 
166
    def test_no_such_id(self):
 
167
        error = errors.NoSuchId("atree", "anid")
 
168
        self.assertEqualDiff("The file id \"anid\" is not present in the tree "
 
169
            "atree.",
 
170
            str(error))
 
171
 
 
172
    def test_no_such_revision_in_tree(self):
 
173
        error = errors.NoSuchRevisionInTree("atree", "anid")
 
174
        self.assertEqualDiff("The revision id {anid} is not present in the"
 
175
                             " tree atree.", str(error))
 
176
        self.assertIsInstance(error, errors.NoSuchRevision)
 
177
 
 
178
    def test_not_stacked(self):
 
179
        error = errors.NotStacked('a branch')
 
180
        self.assertEqualDiff("The branch 'a branch' is not stacked.",
 
181
            str(error))
 
182
 
 
183
    def test_not_write_locked(self):
 
184
        error = errors.NotWriteLocked('a thing to repr')
 
185
        self.assertEqualDiff("'a thing to repr' is not write locked but needs "
 
186
            "to be.",
 
187
            str(error))
 
188
 
 
189
    def test_lock_failed(self):
 
190
        error = errors.LockFailed('http://canonical.com/', 'readonly transport')
 
191
        self.assertEqualDiff("Cannot lock http://canonical.com/: readonly transport",
 
192
            str(error))
 
193
        self.assertFalse(error.internal_error)
 
194
 
 
195
    def test_too_many_concurrent_requests(self):
 
196
        error = errors.TooManyConcurrentRequests("a medium")
 
197
        self.assertEqualDiff("The medium 'a medium' has reached its concurrent "
 
198
            "request limit. Be sure to finish_writing and finish_reading on "
 
199
            "the currently open request.",
 
200
            str(error))
 
201
 
 
202
    def test_unavailable_representation(self):
 
203
        error = errors.UnavailableRepresentation(('key',), "mpdiff", "fulltext")
 
204
        self.assertEqualDiff("The encoding 'mpdiff' is not available for key "
 
205
            "('key',) which is encoded as 'fulltext'.",
 
206
            str(error))
 
207
 
 
208
    def test_unknown_hook(self):
 
209
        error = errors.UnknownHook("branch", "foo")
 
210
        self.assertEqualDiff("The branch hook 'foo' is unknown in this version"
 
211
            " of bzrlib.",
 
212
            str(error))
 
213
        error = errors.UnknownHook("tree", "bar")
 
214
        self.assertEqualDiff("The tree hook 'bar' is unknown in this version"
 
215
            " of bzrlib.",
 
216
            str(error))
 
217
 
 
218
    def test_unstackable_branch_format(self):
 
219
        format = u'foo'
 
220
        url = "/foo"
 
221
        error = errors.UnstackableBranchFormat(format, url)
 
222
        self.assertEqualDiff(
 
223
            "The branch '/foo'(foo) is not a stackable format. "
 
224
            "You will need to upgrade the branch to permit branch stacking.",
 
225
            str(error))
 
226
 
 
227
    def test_unstackable_repository_format(self):
 
228
        format = u'foo'
 
229
        url = "/foo"
 
230
        error = errors.UnstackableRepositoryFormat(format, url)
 
231
        self.assertEqualDiff(
 
232
            "The repository '/foo'(foo) is not a stackable format. "
 
233
            "You will need to upgrade the repository to permit branch stacking.",
 
234
            str(error))
32
235
 
33
236
    def test_up_to_date(self):
34
237
        error = errors.UpToDateFormat(bzrdir.BzrDirFormat4())
44
247
                             "Please run bzr reconcile on this repository." %
45
248
                             repo.bzrdir.root_transport.base,
46
249
                             str(error))
 
250
 
 
251
    def test_read_error(self):
 
252
        # a unicode path to check that %r is being used.
 
253
        path = u'a path'
 
254
        error = errors.ReadError(path)
 
255
        self.assertEqualDiff("Error reading from u'a path'.", str(error))
 
256
 
 
257
    def test_bad_index_format_signature(self):
 
258
        error = errors.BadIndexFormatSignature("foo", "bar")
 
259
        self.assertEqual("foo is not an index of type bar.",
 
260
            str(error))
 
261
 
 
262
    def test_bad_index_data(self):
 
263
        error = errors.BadIndexData("foo")
 
264
        self.assertEqual("Error in data for index foo.",
 
265
            str(error))
 
266
 
 
267
    def test_bad_index_duplicate_key(self):
 
268
        error = errors.BadIndexDuplicateKey("foo", "bar")
 
269
        self.assertEqual("The key 'foo' is already in index 'bar'.",
 
270
            str(error))
 
271
 
 
272
    def test_bad_index_key(self):
 
273
        error = errors.BadIndexKey("foo")
 
274
        self.assertEqual("The key 'foo' is not a valid key.",
 
275
            str(error))
 
276
 
 
277
    def test_bad_index_options(self):
 
278
        error = errors.BadIndexOptions("foo")
 
279
        self.assertEqual("Could not parse options for index foo.",
 
280
            str(error))
 
281
 
 
282
    def test_bad_index_value(self):
 
283
        error = errors.BadIndexValue("foo")
 
284
        self.assertEqual("The value 'foo' is not a valid value.",
 
285
            str(error))
 
286
 
 
287
    def test_bzrnewerror_is_deprecated(self):
 
288
        class DeprecatedError(errors.BzrNewError):
 
289
            pass
 
290
        self.callDeprecated(['BzrNewError was deprecated in bzr 0.13; '
 
291
             'please convert DeprecatedError to use BzrError instead'],
 
292
            DeprecatedError)
 
293
 
 
294
    def test_bzrerror_from_literal_string(self):
 
295
        # Some code constructs BzrError from a literal string, in which case
 
296
        # no further formatting is done.  (I'm not sure raising the base class
 
297
        # is a great idea, but if the exception is not intended to be caught
 
298
        # perhaps no more is needed.)
 
299
        try:
 
300
            raise errors.BzrError('this is my errors; %d is not expanded')
 
301
        except errors.BzrError, e:
 
302
            self.assertEqual('this is my errors; %d is not expanded', str(e))
 
303
 
 
304
    def test_reading_completed(self):
 
305
        error = errors.ReadingCompleted("a request")
 
306
        self.assertEqualDiff("The MediumRequest 'a request' has already had "
 
307
            "finish_reading called upon it - the request has been completed and"
 
308
            " no more data may be read.",
 
309
            str(error))
 
310
 
 
311
    def test_writing_completed(self):
 
312
        error = errors.WritingCompleted("a request")
 
313
        self.assertEqualDiff("The MediumRequest 'a request' has already had "
 
314
            "finish_writing called upon it - accept bytes may not be called "
 
315
            "anymore.",
 
316
            str(error))
 
317
 
 
318
    def test_writing_not_completed(self):
 
319
        error = errors.WritingNotComplete("a request")
 
320
        self.assertEqualDiff("The MediumRequest 'a request' has not has "
 
321
            "finish_writing called upon it - until the write phase is complete"
 
322
            " no data may be read.",
 
323
            str(error))
 
324
 
 
325
    def test_transport_not_possible(self):
 
326
        error = errors.TransportNotPossible('readonly', 'original error')
 
327
        self.assertEqualDiff('Transport operation not possible:'
 
328
                         ' readonly original error', str(error))
 
329
 
 
330
    def assertSocketConnectionError(self, expected, *args, **kwargs):
 
331
        """Check the formatting of a SocketConnectionError exception"""
 
332
        e = errors.SocketConnectionError(*args, **kwargs)
 
333
        self.assertEqual(expected, str(e))
 
334
 
 
335
    def test_socket_connection_error(self):
 
336
        """Test the formatting of SocketConnectionError"""
 
337
 
 
338
        # There should be a default msg about failing to connect
 
339
        # we only require a host name.
 
340
        self.assertSocketConnectionError(
 
341
            'Failed to connect to ahost',
 
342
            'ahost')
 
343
 
 
344
        # If port is None, we don't put :None
 
345
        self.assertSocketConnectionError(
 
346
            'Failed to connect to ahost',
 
347
            'ahost', port=None)
 
348
        # But if port is supplied we include it
 
349
        self.assertSocketConnectionError(
 
350
            'Failed to connect to ahost:22',
 
351
            'ahost', port=22)
 
352
 
 
353
        # We can also supply extra information about the error
 
354
        # with or without a port
 
355
        self.assertSocketConnectionError(
 
356
            'Failed to connect to ahost:22; bogus error',
 
357
            'ahost', port=22, orig_error='bogus error')
 
358
        self.assertSocketConnectionError(
 
359
            'Failed to connect to ahost; bogus error',
 
360
            'ahost', orig_error='bogus error')
 
361
        # An exception object can be passed rather than a string
 
362
        orig_error = ValueError('bad value')
 
363
        self.assertSocketConnectionError(
 
364
            'Failed to connect to ahost; %s' % (str(orig_error),),
 
365
            host='ahost', orig_error=orig_error)
 
366
 
 
367
        # And we can supply a custom failure message
 
368
        self.assertSocketConnectionError(
 
369
            'Unable to connect to ssh host ahost:444; my_error',
 
370
            host='ahost', port=444, msg='Unable to connect to ssh host',
 
371
            orig_error='my_error')
 
372
 
 
373
    def test_malformed_bug_identifier(self):
 
374
        """Test the formatting of MalformedBugIdentifier."""
 
375
        error = errors.MalformedBugIdentifier('bogus', 'reason for bogosity')
 
376
        self.assertEqual(
 
377
            "Did not understand bug identifier bogus: reason for bogosity",
 
378
            str(error))
 
379
 
 
380
    def test_unknown_bug_tracker_abbreviation(self):
 
381
        """Test the formatting of UnknownBugTrackerAbbreviation."""
 
382
        branch = self.make_branch('some_branch')
 
383
        error = errors.UnknownBugTrackerAbbreviation('xxx', branch)
 
384
        self.assertEqual(
 
385
            "Cannot find registered bug tracker called xxx on %s" % branch,
 
386
            str(error))
 
387
 
 
388
    def test_unexpected_smart_server_response(self):
 
389
        e = errors.UnexpectedSmartServerResponse(('not yes',))
 
390
        self.assertEqual(
 
391
            "Could not understand response from smart server: ('not yes',)",
 
392
            str(e))
 
393
 
 
394
    def test_unknown_container_format(self):
 
395
        """Test the formatting of UnknownContainerFormatError."""
 
396
        e = errors.UnknownContainerFormatError('bad format string')
 
397
        self.assertEqual(
 
398
            "Unrecognised container format: 'bad format string'",
 
399
            str(e))
 
400
 
 
401
    def test_unexpected_end_of_container(self):
 
402
        """Test the formatting of UnexpectedEndOfContainerError."""
 
403
        e = errors.UnexpectedEndOfContainerError()
 
404
        self.assertEqual(
 
405
            "Unexpected end of container stream", str(e))
 
406
 
 
407
    def test_unknown_record_type(self):
 
408
        """Test the formatting of UnknownRecordTypeError."""
 
409
        e = errors.UnknownRecordTypeError("X")
 
410
        self.assertEqual(
 
411
            "Unknown record type: 'X'",
 
412
            str(e))
 
413
 
 
414
    def test_invalid_record(self):
 
415
        """Test the formatting of InvalidRecordError."""
 
416
        e = errors.InvalidRecordError("xxx")
 
417
        self.assertEqual(
 
418
            "Invalid record: xxx",
 
419
            str(e))
 
420
 
 
421
    def test_container_has_excess_data(self):
 
422
        """Test the formatting of ContainerHasExcessDataError."""
 
423
        e = errors.ContainerHasExcessDataError("excess bytes")
 
424
        self.assertEqual(
 
425
            "Container has data after end marker: 'excess bytes'",
 
426
            str(e))
 
427
 
 
428
    def test_duplicate_record_name_error(self):
 
429
        """Test the formatting of DuplicateRecordNameError."""
 
430
        e = errors.DuplicateRecordNameError(u"n\xe5me".encode('utf-8'))
 
431
        self.assertEqual(
 
432
            "Container has multiple records with the same name: n\xc3\xa5me",
 
433
            str(e))
 
434
        
 
435
    def test_check_error(self):
 
436
        # This has a member called 'message', which is problematic in
 
437
        # python2.5 because that is a slot on the base Exception class
 
438
        e = errors.BzrCheckError('example check failure')
 
439
        self.assertEqual(
 
440
            "Internal check failed: example check failure",
 
441
            str(e))
 
442
        self.assertTrue(e.internal_error)
 
443
 
 
444
    def test_repository_data_stream_error(self):
 
445
        """Test the formatting of RepositoryDataStreamError."""
 
446
        e = errors.RepositoryDataStreamError(u"my reason")
 
447
        self.assertEqual(
 
448
            "Corrupt or incompatible data stream: my reason", str(e))
 
449
 
 
450
    def test_immortal_pending_deletion_message(self):
 
451
        err = errors.ImmortalPendingDeletion('foo')
 
452
        self.assertEquals(
 
453
            "Unable to delete transform temporary directory foo.  "
 
454
            "Please examine foo to see if it contains any files "
 
455
            "you wish to keep, and delete it when you are done.",
 
456
            str(err))
 
457
 
 
458
    def test_unable_create_symlink(self):
 
459
        err = errors.UnableCreateSymlink()
 
460
        self.assertEquals(
 
461
            "Unable to create symlink on this platform",
 
462
            str(err))
 
463
        err = errors.UnableCreateSymlink(path=u'foo')
 
464
        self.assertEquals(
 
465
            "Unable to create symlink 'foo' on this platform",
 
466
            str(err))
 
467
        err = errors.UnableCreateSymlink(path=u'\xb5')
 
468
        self.assertEquals(
 
469
            "Unable to create symlink u'\\xb5' on this platform",
 
470
            str(err))
 
471
 
 
472
    def test_invalid_url_join(self):
 
473
        """Test the formatting of InvalidURLJoin."""
 
474
        e = errors.InvalidURLJoin('Reason', 'base path', ('args',))
 
475
        self.assertEqual(
 
476
            "Invalid URL join request: Reason: 'base path' + ('args',)",
 
477
            str(e))
 
478
 
 
479
    def test_incorrect_url(self):
 
480
        err = errors.InvalidBugTrackerURL('foo', 'http://bug.com/')
 
481
        self.assertEquals(
 
482
            ("The URL for bug tracker \"foo\" doesn't contain {id}: "
 
483
             "http://bug.com/"),
 
484
            str(err))
 
485
 
 
486
    def test_unable_encode_path(self):
 
487
        err = errors.UnableEncodePath('foo', 'executable')
 
488
        self.assertEquals("Unable to encode executable path 'foo' in "
 
489
            "user encoding " + osutils.get_user_encoding(),
 
490
            str(err))
 
491
 
 
492
    def test_unknown_format(self):
 
493
        err = errors.UnknownFormatError('bar', kind='foo')
 
494
        self.assertEquals("Unknown foo format: 'bar'", str(err))
 
495
 
 
496
 
 
497
class PassThroughError(errors.BzrError):
 
498
    
 
499
    _fmt = """Pass through %(foo)s and %(bar)s"""
 
500
 
 
501
    def __init__(self, foo, bar):
 
502
        errors.BzrError.__init__(self, foo=foo, bar=bar)
 
503
 
 
504
 
 
505
class ErrorWithBadFormat(errors.BzrError):
 
506
 
 
507
    _fmt = """One format specifier: %(thing)s"""
 
508
 
 
509
 
 
510
class ErrorWithNoFormat(errors.BzrError):
 
511
    """This class has a docstring but no format string."""
 
512
 
 
513
 
 
514
class TestErrorFormatting(TestCase):
 
515
    
 
516
    def test_always_str(self):
 
517
        e = PassThroughError(u'\xb5', 'bar')
 
518
        self.assertIsInstance(e.__str__(), str)
 
519
        # In Python str(foo) *must* return a real byte string
 
520
        # not a Unicode string. The following line would raise a
 
521
        # Unicode error, because it tries to call str() on the string
 
522
        # returned from e.__str__(), and it has non ascii characters
 
523
        s = str(e)
 
524
        self.assertEqual('Pass through \xc2\xb5 and bar', s)
 
525
 
 
526
    def test_missing_format_string(self):
 
527
        e = ErrorWithNoFormat(param='randomvalue')
 
528
        s = self.callDeprecated(
 
529
                ['ErrorWithNoFormat uses its docstring as a format, it should use _fmt instead'],
 
530
                lambda x: str(x), e)
 
531
        ## s = str(e)
 
532
        self.assertEqual(s, 
 
533
                "This class has a docstring but no format string.")
 
534
 
 
535
    def test_mismatched_format_args(self):
 
536
        # Even though ErrorWithBadFormat's format string does not match the
 
537
        # arguments we constructing it with, we can still stringify an instance
 
538
        # of this exception. The resulting string will say its unprintable.
 
539
        e = ErrorWithBadFormat(not_thing='x')
 
540
        self.assertStartsWith(
 
541
            str(e), 'Unprintable exception ErrorWithBadFormat')