88
96
from bzrlib.symbol_versioning import (
89
97
DEPRECATED_PARAMETER,
90
98
deprecated_function,
91
100
deprecated_method,
92
101
deprecated_passed,
94
103
import bzrlib.trace
95
from bzrlib.transport import get_transport, pathfilter
104
from bzrlib.transport import (
96
109
import bzrlib.transport
97
from bzrlib.transport.local import LocalURLServer
98
from bzrlib.transport.memory import MemoryServer
99
from bzrlib.transport.readonly import ReadonlyServer
100
110
from bzrlib.trace import mutter, note
101
from bzrlib.tests import TestUtil
111
from bzrlib.tests import (
102
115
from bzrlib.tests.http_server import HttpServer
103
116
from bzrlib.tests.TestUtil import (
228
246
'%d non-main threads were left active in the end.\n'
229
247
% (TestCase._active_threads - 1))
231
def _extractBenchmarkTime(self, testCase):
249
def getDescription(self, test):
252
def _extractBenchmarkTime(self, testCase, details=None):
232
253
"""Add a benchmark time for the current test case."""
254
if details and 'benchtime' in details:
255
return float(''.join(details['benchtime'].iter_bytes()))
233
256
return getattr(testCase, "_benchtime", None)
235
258
def _elapsedTestTimeString(self):
321
344
self._cleanupLogFile(test)
323
def addSuccess(self, test):
346
def addSuccess(self, test, details=None):
324
347
"""Tell result that test completed successfully.
326
349
Called from the TestCase run()
328
351
if self._bench_history is not None:
329
benchmark_time = self._extractBenchmarkTime(test)
352
benchmark_time = self._extractBenchmarkTime(test, details)
330
353
if benchmark_time is not None:
331
354
self._bench_history.write("%s %s\n" % (
332
355
self._formatTime(benchmark_time),
362
385
self.not_applicable_count += 1
363
386
self.report_not_applicable(test, reason)
365
def printErrorList(self, flavour, errors):
366
for test, err in errors:
367
self.stream.writeln(self.separator1)
368
self.stream.write("%s: " % flavour)
369
self.stream.writeln(self.getDescription(test))
370
if getattr(test, '_get_log', None) is not None:
371
log_contents = test._get_log()
373
self.stream.write('\n')
375
('vvvv[log from %s]' % test.id()).ljust(78,'-'))
376
self.stream.write('\n')
377
self.stream.write(log_contents)
378
self.stream.write('\n')
380
('^^^^[log from %s]' % test.id()).ljust(78,'-'))
381
self.stream.write('\n')
382
self.stream.writeln(self.separator2)
383
self.stream.writeln("%s" % err)
385
388
def _post_mortem(self):
386
389
"""Start a PDB post mortem session."""
387
390
if os.environ.get('BZR_TEST_PDB', None):
487
489
return self._shortened_test_description(test)
489
491
def report_error(self, test, err):
490
ui.ui_factory.note('ERROR: %s\n %s\n' % (
492
self.ui.note('ERROR: %s\n %s\n' % (
491
493
self._test_description(test),
495
497
def report_failure(self, test, err):
496
ui.ui_factory.note('FAIL: %s\n %s\n' % (
498
self.ui.note('FAIL: %s\n %s\n' % (
497
499
self._test_description(test),
501
503
def report_known_failure(self, test, err):
502
ui.ui_factory.note('XFAIL: %s\n%s\n' % (
503
self._test_description(test), err[1]))
505
506
def report_skip(self, test, reason):
604
605
applied left to right - the first element in the list is the
605
606
innermost decorator.
608
# stream may know claim to know to write unicode strings, but in older
609
# pythons this goes sufficiently wrong that it is a bad idea. (
610
# specifically a built in file with encoding 'UTF-8' will still try
611
# to encode using ascii.
612
new_encoding = osutils.get_terminal_encoding()
613
codec = codecs.lookup(new_encoding)
614
if type(codec) is tuple:
618
encode = codec.encode
619
stream = osutils.UnicodeOrBytesToBytesWriter(encode, stream)
620
stream.encoding = new_encoding
607
621
self.stream = unittest._WritelnDecorator(stream)
608
622
self.descriptions = descriptions
609
623
self.verbosity = verbosity
676
class KnownFailure(AssertionError):
677
"""Indicates that a test failed in a precisely expected manner.
679
Such failures dont block the whole test suite from passing because they are
680
indicators of partially completed code or of future work. We have an
681
explicit error for them so that we can ensure that they are always visible:
682
KnownFailures are always shown in the output of bzr selftest.
681
# traceback._some_str fails to format exceptions that have the default
682
# __str__ which does an implicit ascii conversion. However, repr() on those
683
# objects works, for all that its not quite what the doctor may have ordered.
684
def _clever_some_str(value):
689
return repr(value).replace('\\n', '\n')
691
return '<unprintable %s object>' % type(value).__name__
693
traceback._some_str = _clever_some_str
696
# deprecated - use self.knownFailure(), or self.expectFailure.
697
KnownFailure = testtools.testcase._ExpectedFailure
686
700
class UnavailableFeature(Exception):
787
797
_leaking_threads_tests = 0
788
798
_first_thread_leaker_id = None
789
799
_log_file_name = None
791
_keep_log_file = False
792
800
# record lsprof data when performing benchmark calls.
793
801
_gather_lsprof_in_benchmarks = False
794
attrs_to_keep = ('id', '_testMethodName', '_testMethodDoc',
795
'_log_contents', '_log_file_name', '_benchtime',
796
'_TestCase__testMethodName', '_TestCase__testMethodDoc',)
798
803
def __init__(self, methodName='testMethod'):
799
804
super(TestCase, self).__init__(methodName)
800
805
self._cleanups = []
801
self._bzr_test_setUp_run = False
802
self._bzr_test_tearDown_run = False
803
806
self._directory_isolation = True
807
self.exception_handlers.insert(0,
808
(UnavailableFeature, self._do_unsupported_or_skip))
809
self.exception_handlers.insert(0,
810
(TestNotApplicable, self._do_not_applicable))
806
unittest.TestCase.setUp(self)
807
self._bzr_test_setUp_run = True
813
super(TestCase, self).setUp()
814
for feature in getattr(self, '_test_needs_features', []):
815
self.requireFeature(feature)
816
self._log_contents = None
817
self.addDetail("log", content.Content(content.ContentType("text",
818
"plain", {"charset": "utf8"}),
819
lambda:[self._get_log(keep_log_file=True)]))
808
820
self._cleanEnvironment()
809
821
self._silenceUI()
810
822
self._startLogFile()
843
855
Tests that want to use debug flags can just set them in the
844
856
debug_flags set during setup/teardown.
846
self._preserved_debug_flags = set(debug.debug_flags)
858
# Start with a copy of the current debug flags we can safely modify.
859
self.overrideAttr(debug, 'debug_flags', set(debug.debug_flags))
847
860
if 'allow_debug' not in selftest_debug_flags:
848
861
debug.debug_flags.clear()
849
862
if 'disable_lock_checks' not in selftest_debug_flags:
850
863
debug.debug_flags.add('strict_locks')
851
self.addCleanup(self._restore_debug_flags)
853
865
def _clear_hooks(self):
854
866
# prevent hooks affecting tests
1013
1021
server's urls to be used.
1015
1023
if backing_server is None:
1016
transport_server.setUp()
1024
transport_server.start_server()
1018
transport_server.setUp(backing_server)
1019
self.addCleanup(transport_server.tearDown)
1026
transport_server.start_server(backing_server)
1027
self.addCleanup(transport_server.stop_server)
1020
1028
# Obtain a real transport because if the server supplies a password, it
1021
1029
# will be hidden from the base on the client side.
1022
1030
t = get_transport(transport_server.get_url())
1036
1044
if t.base.endswith('/work/'):
1037
1045
# we have safety net/test root/work
1038
1046
t = t.clone('../..')
1039
elif isinstance(transport_server, server.SmartTCPServer_for_testing):
1047
elif isinstance(transport_server,
1048
test_server.SmartTCPServer_for_testing):
1040
1049
# The smart server adds a path similar to work, which is traversed
1041
1050
# up from by the client. But the server is chrooted - the actual
1042
1051
# backing transport is not escaped from, and VFS requests to the
1289
1302
m += ": " + msg
1292
def expectFailure(self, reason, assertion, *args, **kwargs):
1293
"""Invoke a test, expecting it to fail for the given reason.
1295
This is for assertions that ought to succeed, but currently fail.
1296
(The failure is *expected* but not *wanted*.) Please be very precise
1297
about the failure you're expecting. If a new bug is introduced,
1298
AssertionError should be raised, not KnownFailure.
1300
Frequently, expectFailure should be followed by an opposite assertion.
1303
Intended to be used with a callable that raises AssertionError as the
1304
'assertion' parameter. args and kwargs are passed to the 'assertion'.
1306
Raises KnownFailure if the test fails. Raises AssertionError if the
1311
self.expectFailure('Math is broken', self.assertNotEqual, 54,
1313
self.assertEqual(42, dynamic_val)
1315
This means that a dynamic_val of 54 will cause the test to raise
1316
a KnownFailure. Once math is fixed and the expectFailure is removed,
1317
only a dynamic_val of 42 will allow the test to pass. Anything other
1318
than 54 or 42 will cause an AssertionError.
1321
assertion(*args, **kwargs)
1322
except AssertionError:
1323
raise KnownFailure(reason)
1325
self.fail('Unexpected success. Should have failed: %s' % reason)
1327
1305
def assertFileEqual(self, content, path):
1328
1306
"""Fail if path does not contain 'content'."""
1329
1307
self.failUnlessExists(path)
1480
1458
Close the file and delete it, unless setKeepLogfile was called.
1482
if self._log_file is None:
1460
if bzrlib.trace._trace_file:
1461
# flush the log file, to get all content
1462
bzrlib.trace._trace_file.flush()
1484
1463
bzrlib.trace.pop_log_file(self._log_memento)
1485
self._log_file.close()
1486
self._log_file = None
1487
if not self._keep_log_file:
1488
os.remove(self._log_file_name)
1489
self._log_file_name = None
1491
def setKeepLogfile(self):
1492
"""Make the logfile not be deleted when _finishLogFile is called."""
1493
self._keep_log_file = True
1464
# Cache the log result and delete the file on disk
1465
self._get_log(False)
1495
1467
def thisFailsStrictLockCheck(self):
1496
1468
"""It is known that this test would fail with -Dstrict_locks.
1514
1486
self._cleanups.append((callable, args, kwargs))
1488
def overrideAttr(self, obj, attr_name, new=_unitialized_attr):
1489
"""Overrides an object attribute restoring it after the test.
1491
:param obj: The object that will be mutated.
1493
:param attr_name: The attribute name we want to preserve/override in
1496
:param new: The optional value we want to set the attribute to.
1498
:returns: The actual attr value.
1500
value = getattr(obj, attr_name)
1501
# The actual value is captured by the call below
1502
self.addCleanup(setattr, obj, attr_name, value)
1503
if new is not _unitialized_attr:
1504
setattr(obj, attr_name, new)
1516
1507
def _cleanEnvironment(self):
1518
1509
'BZR_HOME': None, # Don't inherit BZR_HOME to all the tests.
1554
1546
'ftp_proxy': None,
1555
1547
'FTP_PROXY': None,
1556
1548
'BZR_REMOTE_PATH': None,
1549
# Generally speaking, we don't want apport reporting on crashes in
1550
# the test envirnoment unless we're specifically testing apport,
1551
# so that it doesn't leak into the real system environment. We
1552
# use an env var so it propagates to subprocesses.
1553
'APPORT_DISABLE': '1',
1559
1556
self.addCleanup(self._restoreEnvironment)
1560
1557
for name, value in new_env.iteritems():
1561
1558
self._captureVar(name, value)
1563
1560
def _captureVar(self, name, newvalue):
1564
1561
"""Set an environment variable, and reset it when finished."""
1565
self.__old_env[name] = osutils.set_or_unset_env(name, newvalue)
1567
def _restore_debug_flags(self):
1568
debug.debug_flags.clear()
1569
debug.debug_flags.update(self._preserved_debug_flags)
1562
self._old_env[name] = osutils.set_or_unset_env(name, newvalue)
1571
1564
def _restoreEnvironment(self):
1572
for name, value in self.__old_env.iteritems():
1565
for name, value in self._old_env.iteritems():
1573
1566
osutils.set_or_unset_env(name, value)
1575
1568
def _restoreHooks(self):
1607
1602
self._do_skip(result, reason)
1609
def _do_unsupported_or_skip(self, result, reason):
1605
def _do_unsupported_or_skip(self, result, e):
1610
1607
addNotSupported = getattr(result, 'addNotSupported', None)
1611
1608
if addNotSupported is not None:
1612
1609
result.addNotSupported(self, reason)
1614
1611
self._do_skip(result, reason)
1616
def run(self, result=None):
1617
if result is None: result = self.defaultTestResult()
1618
result.startTest(self)
1623
result.stopTest(self)
1625
def _run(self, result):
1626
for feature in getattr(self, '_test_needs_features', []):
1627
if not feature.available():
1628
return self._do_unsupported_or_skip(result, feature)
1630
absent_attr = object()
1632
method_name = getattr(self, '_testMethodName', absent_attr)
1633
if method_name is absent_attr:
1635
method_name = getattr(self, '_TestCase__testMethodName')
1636
testMethod = getattr(self, method_name)
1640
if not self._bzr_test_setUp_run:
1642
"test setUp did not invoke "
1643
"bzrlib.tests.TestCase's setUp")
1644
except KeyboardInterrupt:
1647
except KnownFailure:
1648
self._do_known_failure(result)
1651
except TestNotApplicable, e:
1652
self._do_not_applicable(result, e)
1655
except TestSkipped, e:
1656
self._do_skip(result, e.args[0])
1659
except UnavailableFeature, e:
1660
self._do_unsupported_or_skip(result, e.args[0])
1664
result.addError(self, sys.exc_info())
1672
except KnownFailure:
1673
self._do_known_failure(result)
1674
except self.failureException:
1675
result.addFailure(self, sys.exc_info())
1676
except TestNotApplicable, e:
1677
self._do_not_applicable(result, e)
1678
except TestSkipped, e:
1680
reason = "No reason given."
1683
self._do_skip(result, reason)
1684
except UnavailableFeature, e:
1685
self._do_unsupported_or_skip(result, e.args[0])
1686
except KeyboardInterrupt:
1690
result.addError(self, sys.exc_info())
1694
if not self._bzr_test_tearDown_run:
1696
"test tearDown did not invoke "
1697
"bzrlib.tests.TestCase's tearDown")
1698
except KeyboardInterrupt:
1702
result.addError(self, sys.exc_info())
1705
if ok: result.addSuccess(self)
1707
except KeyboardInterrupt:
1712
for attr_name in self.attrs_to_keep:
1713
if attr_name in self.__dict__:
1714
saved_attrs[attr_name] = self.__dict__[attr_name]
1715
self.__dict__ = saved_attrs
1719
self._log_contents = ''
1720
self._bzr_test_tearDown_run = True
1721
unittest.TestCase.tearDown(self)
1723
1613
def time(self, callable, *args, **kwargs):
1724
1614
"""Run callable and accrue the time it takes to the benchmark time.
1743
1635
self._benchtime += time.time() - start
1745
def _runCleanups(self):
1746
"""Run registered cleanup functions.
1748
This should only be called from TestCase.tearDown.
1750
# TODO: Perhaps this should keep running cleanups even if
1751
# one of them fails?
1753
# Actually pop the cleanups from the list so tearDown running
1754
# twice is safe (this happens for skipped tests).
1755
while self._cleanups:
1756
cleanup, args, kwargs = self._cleanups.pop()
1757
cleanup(*args, **kwargs)
1759
1637
def log(self, *args):
1762
1640
def _get_log(self, keep_log_file=False):
1763
"""Get the log from bzrlib.trace calls from this test.
1641
"""Internal helper to get the log from bzrlib.trace for this test.
1643
Please use self.getDetails, or self.get_log to access this in test case
1765
1646
:param keep_log_file: When True, if the log is still a file on disk
1766
1647
leave it as a file on disk. When False, if the log is still a file
1768
1649
self._log_contents.
1769
1650
:return: A string containing the log.
1771
# flush the log file, to get all content
1652
if self._log_contents is not None:
1654
self._log_contents.decode('utf8')
1655
except UnicodeDecodeError:
1656
unicodestr = self._log_contents.decode('utf8', 'replace')
1657
self._log_contents = unicodestr.encode('utf8')
1658
return self._log_contents
1772
1659
import bzrlib.trace
1773
1660
if bzrlib.trace._trace_file:
1661
# flush the log file, to get all content
1774
1662
bzrlib.trace._trace_file.flush()
1775
if self._log_contents:
1776
# XXX: this can hardly contain the content flushed above --vila
1778
return self._log_contents
1779
1663
if self._log_file_name is not None:
1780
1664
logfile = open(self._log_file_name)
1782
1666
log_contents = logfile.read()
1784
1668
logfile.close()
1670
log_contents.decode('utf8')
1671
except UnicodeDecodeError:
1672
unicodestr = log_contents.decode('utf8', 'replace')
1673
log_contents = unicodestr.encode('utf8')
1785
1674
if not keep_log_file:
1676
max_close_attempts = 100
1677
first_close_error = None
1678
while close_attempts < max_close_attempts:
1681
self._log_file.close()
1682
except IOError, ioe:
1683
if ioe.errno is None:
1684
# No errno implies 'close() called during
1685
# concurrent operation on the same file object', so
1686
# retry. Probably a thread is trying to write to
1688
if first_close_error is None:
1689
first_close_error = ioe
1694
if close_attempts > 1:
1696
'Unable to close log file on first attempt, '
1697
'will retry: %s\n' % (first_close_error,))
1698
if close_attempts == max_close_attempts:
1700
'Unable to close log file after %d attempts.\n'
1701
% (max_close_attempts,))
1702
self._log_file = None
1703
# Permit multiple calls to get_log until we clean it up in
1786
1705
self._log_contents = log_contents
1788
1707
os.remove(self._log_file_name)
2164
2091
Tests that expect to provoke LockContention errors should call this.
2166
orig_timeout = bzrlib.lockdir._DEFAULT_TIMEOUT_SECONDS
2168
bzrlib.lockdir._DEFAULT_TIMEOUT_SECONDS = orig_timeout
2169
self.addCleanup(resetTimeout)
2170
bzrlib.lockdir._DEFAULT_TIMEOUT_SECONDS = 0
2093
self.overrideAttr(bzrlib.lockdir, '_DEFAULT_TIMEOUT_SECONDS', 0)
2172
2095
def make_utf8_encoded_stringio(self, encoding_type=None):
2173
2096
"""Return a StringIOWrapper instance, that will encode Unicode
3409
3328
def addFailure(self, test, err):
3410
3329
self.result.addFailure(test, err)
3413
class BZRTransformingResult(ForwardingResult):
3415
def addError(self, test, err):
3416
feature = self._error_looks_like('UnavailableFeature: ', err)
3417
if feature is not None:
3418
self.result.addNotSupported(test, feature)
3420
self.result.addError(test, err)
3422
def addFailure(self, test, err):
3423
known = self._error_looks_like('KnownFailure: ', err)
3424
if known is not None:
3425
self.result.addExpectedFailure(test,
3426
[KnownFailure, KnownFailure(known), None])
3428
self.result.addFailure(test, err)
3430
def _error_looks_like(self, prefix, err):
3431
"""Deserialize exception and returns the stringify value."""
3435
if isinstance(exc, subunit.RemoteException):
3436
# stringify the exception gives access to the remote traceback
3437
# We search the last line for 'prefix'
3438
lines = str(exc).split('\n')
3439
while lines and not lines[-1]:
3442
if lines[-1].startswith(prefix):
3443
value = lines[-1][len(prefix):]
3448
from subunit.test_results import AutoTimingTestResultDecorator
3449
# Expected failure should be seen as a success not a failure Once subunit
3450
# provide native support for that, BZRTransformingResult and this class
3451
# will become useless.
3452
class BzrAutoTimingTestResultDecorator(AutoTimingTestResultDecorator):
3454
def addExpectedFailure(self, test, err):
3455
self._before_event()
3456
return self._call_maybe("addExpectedFailure", self._degrade_skip,
3459
# Let's just define a no-op decorator
3460
BzrAutoTimingTestResultDecorator = lambda x:x
3330
ForwardingResult = testtools.ExtendedToOriginalDecorator
3463
3333
class ProfileResult(ForwardingResult):
3724
3594
# appear prefixed ('bzrlib.' is "replaced" by 'bzrlib.').
3725
3595
test_prefix_alias_registry.register('bzrlib', 'bzrlib')
3727
# Obvious higest levels prefixes, feel free to add your own via a plugin
3597
# Obvious highest levels prefixes, feel free to add your own via a plugin
3728
3598
test_prefix_alias_registry.register('bd', 'bzrlib.doc')
3729
3599
test_prefix_alias_registry.register('bu', 'bzrlib.utils')
3730
3600
test_prefix_alias_registry.register('bt', 'bzrlib.tests')
4138
4011
return new_test
4014
def permute_tests_for_extension(standard_tests, loader, py_module_name,
4016
"""Helper for permutating tests against an extension module.
4018
This is meant to be used inside a modules 'load_tests()' function. It will
4019
create 2 scenarios, and cause all tests in the 'standard_tests' to be run
4020
against both implementations. Setting 'test.module' to the appropriate
4021
module. See bzrlib.tests.test__chk_map.load_tests as an example.
4023
:param standard_tests: A test suite to permute
4024
:param loader: A TestLoader
4025
:param py_module_name: The python path to a python module that can always
4026
be loaded, and will be considered the 'python' implementation. (eg
4027
'bzrlib._chk_map_py')
4028
:param ext_module_name: The python path to an extension module. If the
4029
module cannot be loaded, a single test will be added, which notes that
4030
the module is not available. If it can be loaded, all standard_tests
4031
will be run against that module.
4032
:return: (suite, feature) suite is a test-suite that has all the permuted
4033
tests. feature is the Feature object that can be used to determine if
4034
the module is available.
4037
py_module = __import__(py_module_name, {}, {}, ['NO_SUCH_ATTRIB'])
4039
('python', {'module': py_module}),
4041
suite = loader.suiteClass()
4042
feature = ModuleAvailableFeature(ext_module_name)
4043
if feature.available():
4044
scenarios.append(('C', {'module': feature.module}))
4046
# the compiled module isn't available, so we add a failing test
4047
class FailWithoutFeature(TestCase):
4048
def test_fail(self):
4049
self.requireFeature(feature)
4050
suite.addTest(loader.loadTestsFromTestCase(FailWithoutFeature))
4051
result = multiply_tests(standard_tests, scenarios, suite)
4052
return result, feature
4141
4055
def _rmtree_temp_dir(dirname, test_id=None):
4142
4056
# If LANG=C we probably have created some bogus paths
4143
4057
# which rmtree(unicode) will fail to delete
4248
4162
UnicodeFilenameFeature = _UnicodeFilenameFeature()
4165
class _CompatabilityThunkFeature(Feature):
4166
"""This feature is just a thunk to another feature.
4168
It issues a deprecation warning if it is accessed, to let you know that you
4169
should really use a different feature.
4172
def __init__(self, dep_version, module, name,
4173
replacement_name, replacement_module=None):
4174
super(_CompatabilityThunkFeature, self).__init__()
4175
self._module = module
4176
if replacement_module is None:
4177
replacement_module = module
4178
self._replacement_module = replacement_module
4180
self._replacement_name = replacement_name
4181
self._dep_version = dep_version
4182
self._feature = None
4185
if self._feature is None:
4186
depr_msg = self._dep_version % ('%s.%s'
4187
% (self._module, self._name))
4188
use_msg = ' Use %s.%s instead.' % (self._replacement_module,
4189
self._replacement_name)
4190
symbol_versioning.warn(depr_msg + use_msg, DeprecationWarning)
4191
# Import the new feature and use it as a replacement for the
4193
mod = __import__(self._replacement_module, {}, {},
4194
[self._replacement_name])
4195
self._feature = getattr(mod, self._replacement_name)
4199
return self._feature._probe()
4251
4202
class ModuleAvailableFeature(Feature):
4252
4203
"""This is a feature than describes a module we want to be available.
4460
4400
CaseInsensitiveFilesystemFeature = _CaseInsensitiveFilesystemFeature()
4463
class _SubUnitFeature(Feature):
4464
"""Check if subunit is available."""
4403
class _CaseSensitiveFilesystemFeature(Feature):
4466
4405
def _probe(self):
4406
if CaseInsCasePresFilenameFeature.available():
4408
elif CaseInsensitiveFilesystemFeature.available():
4473
4413
def feature_name(self):
4476
SubUnitFeature = _SubUnitFeature()
4414
return 'case-sensitive filesystem'
4416
# new coding style is for feature instances to be lowercase
4417
case_sensitive_filesystem_feature = _CaseSensitiveFilesystemFeature()
4420
# Kept for compatibility, use bzrlib.tests.features.subunit instead
4421
SubUnitFeature = _CompatabilityThunkFeature(
4422
deprecated_in((2,1,0)),
4423
'bzrlib.tests.features', 'SubUnitFeature', 'subunit')
4477
4424
# Only define SubUnitBzrRunner if subunit is available.
4479
4426
from subunit import TestProtocolClient
4427
from subunit.test_results import AutoTimingTestResultDecorator
4480
4428
class SubUnitBzrRunner(TextTestRunner):
4481
4429
def run(self, test):
4482
result = BzrAutoTimingTestResultDecorator(
4430
result = AutoTimingTestResultDecorator(
4483
4431
TestProtocolClient(self.stream))
4484
4432
test.run(result)