Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f920c21

Browse files
committed
Close #19746: expose unittest discovery errors on TestLoader.errors
This makes it possible to examine the errors from unittest discovery without executing the test suite - important when the test suite may be very large, or when enumerating the test ids from a test suite.
1 parent 1ed2e69 commit f920c21

5 files changed

Lines changed: 70 additions & 9 deletions

File tree

Doc/library/unittest.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,20 @@ Loading and running tests
15521552
:data:`unittest.defaultTestLoader`. Using a subclass or instance, however,
15531553
allows customization of some configurable properties.
15541554

1555+
:class:`TestLoader` objects have the following attributes:
1556+
1557+
1558+
.. attribute:: errors
1559+
1560+
A list of the non-fatal errors encountered while loading tests. Not reset
1561+
by the loader at any point. Fatal errors are signalled by the relevant
1562+
a method raising an exception to the caller. Non-fatal errors are also
1563+
indicated by a synthetic test that will raise the original error when
1564+
run.
1565+
1566+
.. versionadded:: 3.5
1567+
1568+
15551569
:class:`TestLoader` objects have the following methods:
15561570

15571571

Lib/unittest/loader.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,22 @@
2121

2222

2323
def _make_failed_import_test(name, suiteClass):
24-
message = 'Failed to import test module: %s\n%s' % (name, traceback.format_exc())
24+
message = 'Failed to import test module: %s\n%s' % (
25+
name, traceback.format_exc())
2526
return _make_failed_test('ModuleImportFailure', name, ImportError(message),
26-
suiteClass)
27+
suiteClass, message)
2728

2829
def _make_failed_load_tests(name, exception, suiteClass):
29-
return _make_failed_test('LoadTestsFailure', name, exception, suiteClass)
30+
message = 'Failed to call load_tests:\n%s' % (traceback.format_exc(),)
31+
return _make_failed_test(
32+
'LoadTestsFailure', name, exception, suiteClass, message)
3033

31-
def _make_failed_test(classname, methodname, exception, suiteClass):
34+
def _make_failed_test(classname, methodname, exception, suiteClass, message):
3235
def testFailure(self):
3336
raise exception
3437
attrs = {methodname: testFailure}
3538
TestClass = type(classname, (case.TestCase,), attrs)
36-
return suiteClass((TestClass(methodname),))
39+
return suiteClass((TestClass(methodname),)), message
3740

3841
def _make_skipped_test(methodname, exception, suiteClass):
3942
@case.skip(str(exception))
@@ -59,6 +62,10 @@ class TestLoader(object):
5962
suiteClass = suite.TestSuite
6063
_top_level_dir = None
6164

65+
def __init__(self):
66+
super(TestLoader, self).__init__()
67+
self.errors = []
68+
6269
def loadTestsFromTestCase(self, testCaseClass):
6370
"""Return a suite of all tests cases contained in testCaseClass"""
6471
if issubclass(testCaseClass, suite.TestSuite):
@@ -107,8 +114,10 @@ def loadTestsFromModule(self, module, *args, pattern=None, **kws):
107114
try:
108115
return load_tests(self, tests, pattern)
109116
except Exception as e:
110-
return _make_failed_load_tests(module.__name__, e,
111-
self.suiteClass)
117+
error_case, error_message = _make_failed_load_tests(
118+
module.__name__, e, self.suiteClass)
119+
self.errors.append(error_message)
120+
return error_case
112121
return tests
113122

114123
def loadTestsFromName(self, name, module=None):
@@ -336,7 +345,10 @@ def _find_tests(self, start_dir, pattern, namespace=False):
336345
except case.SkipTest as e:
337346
yield _make_skipped_test(name, e, self.suiteClass)
338347
except:
339-
yield _make_failed_import_test(name, self.suiteClass)
348+
error_case, error_message = \
349+
_make_failed_import_test(name, self.suiteClass)
350+
self.errors.append(error_message)
351+
yield error_case
340352
else:
341353
mod_file = os.path.abspath(getattr(module, '__file__', full_path))
342354
realpath = _jython_aware_splitext(os.path.realpath(mod_file))
@@ -362,7 +374,10 @@ def _find_tests(self, start_dir, pattern, namespace=False):
362374
except case.SkipTest as e:
363375
yield _make_skipped_test(name, e, self.suiteClass)
364376
except:
365-
yield _make_failed_import_test(name, self.suiteClass)
377+
error_case, error_message = \
378+
_make_failed_import_test(name, self.suiteClass)
379+
self.errors.append(error_message)
380+
yield error_case
366381
else:
367382
load_tests = getattr(package, 'load_tests', None)
368383
tests = self.loadTestsFromModule(package, pattern=pattern)

Lib/unittest/test/test_discovery.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,13 @@ def test_discover_with_modules_that_fail_to_import(self):
399399
suite = loader.discover('.')
400400
self.assertIn(os.getcwd(), sys.path)
401401
self.assertEqual(suite.countTestCases(), 1)
402+
# Errors loading the suite are also captured for introspection.
403+
self.assertNotEqual([], loader.errors)
404+
self.assertEqual(1, len(loader.errors))
405+
error = loader.errors[0]
406+
self.assertTrue(
407+
'Failed to import test module: test_this_does_not_exist' in error,
408+
'missing error string in %r' % error)
402409
test = list(list(suite)[0])[0] # extract test from suite
403410

404411
with self.assertRaises(ImportError):
@@ -418,6 +425,13 @@ def _get_module_from_name(name):
418425

419426
self.assertIn(abspath('/foo'), sys.path)
420427
self.assertEqual(suite.countTestCases(), 1)
428+
# Errors loading the suite are also captured for introspection.
429+
self.assertNotEqual([], loader.errors)
430+
self.assertEqual(1, len(loader.errors))
431+
error = loader.errors[0]
432+
self.assertTrue(
433+
'Failed to import test module: my_package' in error,
434+
'missing error string in %r' % error)
421435
test = list(list(suite)[0])[0] # extract test from suite
422436
with self.assertRaises(ImportError):
423437
test.my_package()

Lib/unittest/test/test_loader.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ def wrapper(*args, **kws):
2424

2525
class Test_TestLoader(unittest.TestCase):
2626

27+
### Basic object tests
28+
################################################################
29+
30+
def test___init__(self):
31+
loader = unittest.TestLoader()
32+
self.assertEqual([], loader.errors)
33+
2734
### Tests for TestLoader.loadTestsFromTestCase
2835
################################################################
2936

@@ -336,6 +343,13 @@ def load_tests(loader, tests, pattern):
336343
suite = loader.loadTestsFromModule(m)
337344
self.assertIsInstance(suite, unittest.TestSuite)
338345
self.assertEqual(suite.countTestCases(), 1)
346+
# Errors loading the suite are also captured for introspection.
347+
self.assertNotEqual([], loader.errors)
348+
self.assertEqual(1, len(loader.errors))
349+
error = loader.errors[0]
350+
self.assertTrue(
351+
'Failed to call load_tests:' in error,
352+
'missing error string in %r' % error)
339353
test = list(suite)[0]
340354

341355
self.assertRaisesRegex(TypeError, "some failure", test.m)

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ Library
186186
- Issue #9351: Defaults set with set_defaults on an argparse subparser
187187
are no longer ignored when also set on the parent parser.
188188

189+
- Issue #19746: Make it possible to examine the errors from unittest
190+
discovery without executing the test suite. The new `errors` attribute
191+
on TestLoader exposes these non-fatal errors encountered during discovery.
192+
189193
- Issue #21991: Make email.headerregistry's header 'params' attributes
190194
be read-only (MappingProxyType). Previously the dictionary was modifiable
191195
but a new one was created on each access of the attribute.

0 commit comments

Comments
 (0)