Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

runner.py 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  1. # -*- test-case-name: twisted.trial.test.test_runner -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. A miscellany of code used to run Trial tests.
  6. Maintainer: Jonathan Lange
  7. """
  8. __all__ = [
  9. "TestSuite",
  10. "DestructiveTestSuite",
  11. "ErrorHolder",
  12. "LoggedSuite",
  13. "TestHolder",
  14. "TestLoader",
  15. "TrialRunner",
  16. "TrialSuite",
  17. "filenameToModule",
  18. "isPackage",
  19. "isPackageDirectory",
  20. "isTestCase",
  21. "name",
  22. "samefile",
  23. "NOT_IN_TEST",
  24. ]
  25. import doctest
  26. import importlib
  27. import inspect
  28. import os
  29. import sys
  30. import types
  31. import unittest as pyunit
  32. import warnings
  33. from contextlib import contextmanager
  34. from importlib.machinery import SourceFileLoader
  35. from typing import Callable, Generator, List, Optional, TextIO, Type, Union
  36. from zope.interface import implementer
  37. from attrs import define
  38. from typing_extensions import ParamSpec, Protocol, TypeAlias, TypeGuard
  39. from twisted.internet import defer
  40. from twisted.python import failure, filepath, log, modules, reflect
  41. from twisted.trial import unittest, util
  42. from twisted.trial._asyncrunner import _ForceGarbageCollectionDecorator, _iterateTests
  43. from twisted.trial._synctest import _logObserver
  44. from twisted.trial.itrial import ITestCase
  45. from twisted.trial.reporter import UncleanWarningsReporterWrapper, _ExitWrapper
  46. # These are imported so that they remain in the public API for t.trial.runner
  47. from twisted.trial.unittest import TestSuite
  48. from . import itrial
  49. _P = ParamSpec("_P")
  50. class _Debugger(Protocol):
  51. def runcall(
  52. self, f: Callable[_P, object], *args: _P.args, **kwargs: _P.kwargs
  53. ) -> object:
  54. ...
  55. def isPackage(module):
  56. """Given an object return True if the object looks like a package"""
  57. if not isinstance(module, types.ModuleType):
  58. return False
  59. basename = os.path.splitext(os.path.basename(module.__file__))[0]
  60. return basename == "__init__"
  61. def isPackageDirectory(dirname):
  62. """
  63. Is the directory at path 'dirname' a Python package directory?
  64. Returns the name of the __init__ file (it may have a weird extension)
  65. if dirname is a package directory. Otherwise, returns False
  66. """
  67. def _getSuffixes():
  68. return importlib.machinery.all_suffixes()
  69. for ext in _getSuffixes():
  70. initFile = "__init__" + ext
  71. if os.path.exists(os.path.join(dirname, initFile)):
  72. return initFile
  73. return False
  74. def samefile(filename1, filename2):
  75. """
  76. A hacky implementation of C{os.path.samefile}. Used by L{filenameToModule}
  77. when the platform doesn't provide C{os.path.samefile}. Do not use this.
  78. """
  79. return os.path.abspath(filename1) == os.path.abspath(filename2)
  80. def filenameToModule(fn):
  81. """
  82. Given a filename, do whatever possible to return a module object matching
  83. that file.
  84. If the file in question is a module in Python path, properly import and
  85. return that module. Otherwise, load the source manually.
  86. @param fn: A filename.
  87. @return: A module object.
  88. @raise ValueError: If C{fn} does not exist.
  89. """
  90. oldFn = fn
  91. if (3, 8) <= sys.version_info < (3, 10) and not os.path.isabs(fn):
  92. # module.__spec__.__file__ is supposed to be absolute in py3.8+
  93. # importlib.util.spec_from_file_location does this automatically from
  94. # 3.10+
  95. # This was backported to 3.8 and 3.9, but then reverted in 3.8.11 and
  96. # 3.9.6
  97. # See https://twistedmatrix.com/trac/ticket/10230
  98. # and https://bugs.python.org/issue44070
  99. fn = os.path.join(os.getcwd(), fn)
  100. if not os.path.exists(fn):
  101. raise ValueError(f"{oldFn!r} doesn't exist")
  102. moduleName = reflect.filenameToModuleName(fn)
  103. try:
  104. ret = reflect.namedAny(moduleName)
  105. except (ValueError, AttributeError):
  106. # Couldn't find module. The file 'fn' is not in PYTHONPATH
  107. return _importFromFile(fn, moduleName=moduleName)
  108. # >=3.7 has __file__ attribute as None, previously __file__ was not present
  109. if getattr(ret, "__file__", None) is None:
  110. # This isn't a Python module in a package, so import it from a file
  111. return _importFromFile(fn, moduleName=moduleName)
  112. # ensure that the loaded module matches the file
  113. retFile = os.path.splitext(ret.__file__)[0] + ".py"
  114. # not all platforms (e.g. win32) have os.path.samefile
  115. same = getattr(os.path, "samefile", samefile)
  116. if os.path.isfile(fn) and not same(fn, retFile):
  117. del sys.modules[ret.__name__]
  118. ret = _importFromFile(fn, moduleName=moduleName)
  119. return ret
  120. def _importFromFile(fn, *, moduleName):
  121. fn = _resolveDirectory(fn)
  122. if not moduleName:
  123. moduleName = os.path.splitext(os.path.split(fn)[-1])[0]
  124. if moduleName in sys.modules:
  125. return sys.modules[moduleName]
  126. spec = importlib.util.spec_from_file_location(moduleName, fn)
  127. if not spec:
  128. raise SyntaxError(fn)
  129. module = importlib.util.module_from_spec(spec)
  130. spec.loader.exec_module(module)
  131. sys.modules[moduleName] = module
  132. return module
  133. def _resolveDirectory(fn):
  134. if os.path.isdir(fn):
  135. initFile = isPackageDirectory(fn)
  136. if initFile:
  137. fn = os.path.join(fn, initFile)
  138. else:
  139. raise ValueError(f"{fn!r} is not a package directory")
  140. return fn
  141. def _getMethodNameInClass(method):
  142. """
  143. Find the attribute name on the method's class which refers to the method.
  144. For some methods, notably decorators which have not had __name__ set correctly:
  145. getattr(method.im_class, method.__name__) != method
  146. """
  147. if getattr(method.im_class, method.__name__, object()) != method:
  148. for alias in dir(method.im_class):
  149. if getattr(method.im_class, alias, object()) == method:
  150. return alias
  151. return method.__name__
  152. class DestructiveTestSuite(TestSuite):
  153. """
  154. A test suite which remove the tests once run, to minimize memory usage.
  155. """
  156. def run(self, result):
  157. """
  158. Almost the same as L{TestSuite.run}, but with C{self._tests} being
  159. empty at the end.
  160. """
  161. while self._tests:
  162. if result.shouldStop:
  163. break
  164. test = self._tests.pop(0)
  165. test(result)
  166. return result
  167. # When an error occurs outside of any test, the user will see this string
  168. # in place of a test's name.
  169. NOT_IN_TEST = "<not in test>"
  170. class LoggedSuite(TestSuite):
  171. """
  172. Any errors logged in this suite will be reported to the L{TestResult}
  173. object.
  174. """
  175. def run(self, result):
  176. """
  177. Run the suite, storing all errors in C{result}. If an error is logged
  178. while no tests are running, then it will be added as an error to
  179. C{result}.
  180. @param result: A L{TestResult} object.
  181. """
  182. observer = _logObserver
  183. observer._add()
  184. super().run(result)
  185. observer._remove()
  186. for error in observer.getErrors():
  187. result.addError(TestHolder(NOT_IN_TEST), error)
  188. observer.flushErrors()
  189. class TrialSuite(TestSuite):
  190. """
  191. Suite to wrap around every single test in a C{trial} run. Used internally
  192. by Trial to set up things necessary for Trial tests to work, regardless of
  193. what context they are run in.
  194. """
  195. def __init__(self, tests=(), forceGarbageCollection=False):
  196. if forceGarbageCollection:
  197. newTests = []
  198. for test in tests:
  199. test = unittest.decorate(test, _ForceGarbageCollectionDecorator)
  200. newTests.append(test)
  201. tests = newTests
  202. suite = LoggedSuite(tests)
  203. super().__init__([suite])
  204. def _bail(self):
  205. from twisted.internet import reactor
  206. d = defer.Deferred()
  207. reactor.addSystemEventTrigger("after", "shutdown", lambda: d.callback(None))
  208. reactor.fireSystemEvent("shutdown") # radix's suggestion
  209. # As long as TestCase does crap stuff with the reactor we need to
  210. # manually shutdown the reactor here, and that requires util.wait
  211. # :(
  212. # so that the shutdown event completes
  213. unittest.TestCase("mktemp")._wait(d)
  214. def run(self, result):
  215. try:
  216. TestSuite.run(self, result)
  217. finally:
  218. self._bail()
  219. _Loadable: TypeAlias = Union[
  220. modules.PythonAttribute,
  221. modules.PythonModule,
  222. pyunit.TestCase,
  223. Type[pyunit.TestCase],
  224. ]
  225. def name(thing: _Loadable) -> str:
  226. """
  227. @param thing: an object from modules (instance of PythonModule,
  228. PythonAttribute), a TestCase subclass, or an instance of a TestCase.
  229. """
  230. if isinstance(thing, pyunit.TestCase):
  231. return thing.id()
  232. if isinstance(thing, (modules.PythonAttribute, modules.PythonModule)):
  233. return thing.name
  234. if isTestCase(thing):
  235. # TestCase subclass
  236. return reflect.qual(thing)
  237. # Based on the type of thing, this is unreachable. Maybe someone calls
  238. # this from un-type-checked code though. Also, even with the type
  239. # information, mypy fails to determine this is unreachable and complains
  240. # about a missing return without _something_ here.
  241. raise TypeError(f"Cannot name {thing!r}")
  242. def isTestCase(obj: type) -> TypeGuard[Type[pyunit.TestCase]]:
  243. """
  244. @return: C{True} if C{obj} is a class that contains test cases, C{False}
  245. otherwise. Used to find all the tests in a module.
  246. """
  247. try:
  248. return issubclass(obj, pyunit.TestCase)
  249. except TypeError:
  250. return False
  251. @implementer(ITestCase)
  252. class TestHolder:
  253. """
  254. Placeholder for a L{TestCase} inside a reporter. As far as a L{TestResult}
  255. is concerned, this looks exactly like a unit test.
  256. """
  257. failureException = None
  258. def __init__(self, description):
  259. """
  260. @param description: A string to be displayed L{TestResult}.
  261. """
  262. self.description = description
  263. def __call__(self, result):
  264. return self.run(result)
  265. def id(self):
  266. return self.description
  267. def countTestCases(self):
  268. return 0
  269. def run(self, result):
  270. """
  271. This test is just a placeholder. Run the test successfully.
  272. @param result: The C{TestResult} to store the results in.
  273. @type result: L{twisted.trial.itrial.IReporter}.
  274. """
  275. result.startTest(self)
  276. result.addSuccess(self)
  277. result.stopTest(self)
  278. def shortDescription(self):
  279. return self.description
  280. class ErrorHolder(TestHolder):
  281. """
  282. Used to insert arbitrary errors into a test suite run. Provides enough
  283. methods to look like a C{TestCase}, however, when it is run, it simply adds
  284. an error to the C{TestResult}. The most common use-case is for when a
  285. module fails to import.
  286. """
  287. def __init__(self, description, error):
  288. """
  289. @param description: A string used by C{TestResult}s to identify this
  290. error. Generally, this is the name of a module that failed to import.
  291. @param error: The error to be added to the result. Can be an `exc_info`
  292. tuple or a L{twisted.python.failure.Failure}.
  293. """
  294. super().__init__(description)
  295. self.error = util.excInfoOrFailureToExcInfo(error)
  296. def __repr__(self) -> str:
  297. return "<ErrorHolder description={!r} error={!r}>".format(
  298. self.description,
  299. self.error[1],
  300. )
  301. def run(self, result):
  302. """
  303. Run the test, reporting the error.
  304. @param result: The C{TestResult} to store the results in.
  305. @type result: L{twisted.trial.itrial.IReporter}.
  306. """
  307. result.startTest(self)
  308. result.addError(self, self.error)
  309. result.stopTest(self)
  310. @define
  311. class TestLoader:
  312. """
  313. I find tests inside function, modules, files -- whatever -- then return
  314. them wrapped inside a Test (either a L{TestSuite} or a L{TestCase}).
  315. @ivar methodPrefix: A string prefix. C{TestLoader} will assume that all the
  316. methods in a class that begin with C{methodPrefix} are test cases.
  317. @ivar modulePrefix: A string prefix. Every module in a package that begins
  318. with C{modulePrefix} is considered a module full of tests.
  319. @ivar forceGarbageCollection: A flag applied to each C{TestCase} loaded.
  320. See L{unittest.TestCase} for more information.
  321. @ivar sorter: A key function used to sort C{TestCase}s, test classes,
  322. modules and packages.
  323. @ivar suiteFactory: A callable which is passed a list of tests (which
  324. themselves may be suites of tests). Must return a test suite.
  325. """
  326. methodPrefix = "test"
  327. modulePrefix = "test_"
  328. suiteFactory: Type[TestSuite] = TestSuite
  329. sorter: Callable[[_Loadable], object] = name
  330. def sort(self, xs):
  331. """
  332. Sort the given things using L{sorter}.
  333. @param xs: A list of test cases, class or modules.
  334. """
  335. return sorted(xs, key=self.sorter)
  336. def findTestClasses(self, module):
  337. """Given a module, return all Trial test classes"""
  338. classes = []
  339. for name, val in inspect.getmembers(module):
  340. if isTestCase(val):
  341. classes.append(val)
  342. return self.sort(classes)
  343. def findByName(self, _name, recurse=False):
  344. """
  345. Find and load tests, given C{name}.
  346. @param _name: The qualified name of the thing to load.
  347. @param recurse: A boolean. If True, inspect modules within packages
  348. within the given package (and so on), otherwise, only inspect
  349. modules in the package itself.
  350. @return: If C{name} is a filename, return the module. If C{name} is a
  351. fully-qualified Python name, return the object it refers to.
  352. """
  353. if os.sep in _name:
  354. # It's a file, try and get the module name for this file.
  355. name = reflect.filenameToModuleName(_name)
  356. try:
  357. # Try and import it, if it's on the path.
  358. # CAVEAT: If you have two twisteds, and you try and import the
  359. # one NOT on your path, it'll load the one on your path. But
  360. # that's silly, nobody should do that, and existing Trial does
  361. # that anyway.
  362. __import__(name)
  363. except ImportError:
  364. # If we can't import it, look for one NOT on the path.
  365. return self.loadFile(_name, recurse=recurse)
  366. else:
  367. name = _name
  368. obj = parent = remaining = None
  369. for searchName, remainingName in _qualNameWalker(name):
  370. # Walk down the qualified name, trying to import a module. For
  371. # example, `twisted.test.test_paths.FilePathTests` would try
  372. # the full qualified name, then just up to test_paths, and then
  373. # just up to test, and so forth.
  374. # This gets us the highest level thing which is a module.
  375. try:
  376. obj = reflect.namedModule(searchName)
  377. # If we reach here, we have successfully found a module.
  378. # obj will be the module, and remaining will be the remaining
  379. # part of the qualified name.
  380. remaining = remainingName
  381. break
  382. except ImportError:
  383. # Check to see where the ImportError happened. If it happened
  384. # in this file, ignore it.
  385. tb = sys.exc_info()[2]
  386. # Walk down to the deepest frame, where it actually happened.
  387. while tb.tb_next is not None:
  388. tb = tb.tb_next
  389. # Get the filename that the ImportError originated in.
  390. filenameWhereHappened = tb.tb_frame.f_code.co_filename
  391. # If it originated in the reflect file, then it's because it
  392. # doesn't exist. If it originates elsewhere, it's because an
  393. # ImportError happened in a module that does exist.
  394. if filenameWhereHappened != reflect.__file__:
  395. raise
  396. if remaining == "":
  397. raise reflect.ModuleNotFound(f"The module {name} does not exist.")
  398. if obj is None:
  399. # If it's none here, we didn't get to import anything.
  400. # Try something drastic.
  401. obj = reflect.namedAny(name)
  402. remaining = name.split(".")[len(".".split(obj.__name__)) + 1 :]
  403. try:
  404. for part in remaining:
  405. # Walk down the remaining modules. Hold on to the parent for
  406. # methods, as on Python 3, you can no longer get the parent
  407. # class from just holding onto the method.
  408. parent, obj = obj, getattr(obj, part)
  409. except AttributeError:
  410. raise AttributeError(f"{name} does not exist.")
  411. return self.loadAnything(
  412. obj, parent=parent, qualName=remaining, recurse=recurse
  413. )
  414. def loadModule(self, module):
  415. """
  416. Return a test suite with all the tests from a module.
  417. Included are TestCase subclasses and doctests listed in the module's
  418. __doctests__ module. If that's not good for you, put a function named
  419. either C{testSuite} or C{test_suite} in your module that returns a
  420. TestSuite, and I'll use the results of that instead.
  421. If C{testSuite} and C{test_suite} are both present, then I'll use
  422. C{testSuite}.
  423. """
  424. ## XXX - should I add an optional parameter to disable the check for
  425. ## a custom suite.
  426. ## OR, should I add another method
  427. if not isinstance(module, types.ModuleType):
  428. raise TypeError(f"{module!r} is not a module")
  429. if hasattr(module, "testSuite"):
  430. return module.testSuite()
  431. elif hasattr(module, "test_suite"):
  432. return module.test_suite()
  433. suite = self.suiteFactory()
  434. for testClass in self.findTestClasses(module):
  435. suite.addTest(self.loadClass(testClass))
  436. if not hasattr(module, "__doctests__"):
  437. return suite
  438. docSuite = self.suiteFactory()
  439. for docTest in module.__doctests__:
  440. docSuite.addTest(self.loadDoctests(docTest))
  441. return self.suiteFactory([suite, docSuite])
  442. loadTestsFromModule = loadModule
  443. def loadClass(self, klass):
  444. """
  445. Given a class which contains test cases, return a list of L{TestCase}s.
  446. @param klass: The class to load tests from.
  447. """
  448. if not isinstance(klass, type):
  449. raise TypeError(f"{klass!r} is not a class")
  450. if not isTestCase(klass):
  451. raise ValueError(f"{klass!r} is not a test case")
  452. names = self.getTestCaseNames(klass)
  453. tests = self.sort(
  454. [self._makeCase(klass, self.methodPrefix + name) for name in names]
  455. )
  456. return self.suiteFactory(tests)
  457. loadTestsFromTestCase = loadClass
  458. def getTestCaseNames(self, klass):
  459. """
  460. Given a class that contains C{TestCase}s, return a list of names of
  461. methods that probably contain tests.
  462. """
  463. return reflect.prefixedMethodNames(klass, self.methodPrefix)
  464. def _makeCase(self, klass, methodName):
  465. return klass(methodName)
  466. def loadPackage(self, package, recurse=False):
  467. """
  468. Load tests from a module object representing a package, and return a
  469. TestSuite containing those tests.
  470. Tests are only loaded from modules whose name begins with 'test_'
  471. (or whatever C{modulePrefix} is set to).
  472. @param package: a types.ModuleType object (or reasonable facsimile
  473. obtained by importing) which may contain tests.
  474. @param recurse: A boolean. If True, inspect modules within packages
  475. within the given package (and so on), otherwise, only inspect modules
  476. in the package itself.
  477. @raise TypeError: If C{package} is not a package.
  478. @return: a TestSuite created with my suiteFactory, containing all the
  479. tests.
  480. """
  481. if not isPackage(package):
  482. raise TypeError(f"{package!r} is not a package")
  483. pkgobj = modules.getModule(package.__name__)
  484. if recurse:
  485. discovery = pkgobj.walkModules()
  486. else:
  487. discovery = pkgobj.iterModules()
  488. discovered = []
  489. for disco in discovery:
  490. if disco.name.split(".")[-1].startswith(self.modulePrefix):
  491. discovered.append(disco)
  492. suite = self.suiteFactory()
  493. for modinfo in self.sort(discovered):
  494. try:
  495. module = modinfo.load()
  496. except BaseException:
  497. thingToAdd = ErrorHolder(modinfo.name, failure.Failure())
  498. else:
  499. thingToAdd = self.loadModule(module)
  500. suite.addTest(thingToAdd)
  501. return suite
  502. def loadDoctests(self, module):
  503. """
  504. Return a suite of tests for all the doctests defined in C{module}.
  505. @param module: A module object or a module name.
  506. """
  507. if isinstance(module, str):
  508. try:
  509. module = reflect.namedAny(module)
  510. except BaseException:
  511. return ErrorHolder(module, failure.Failure())
  512. if not inspect.ismodule(module):
  513. warnings.warn("trial only supports doctesting modules")
  514. return
  515. extraArgs = {}
  516. # Work around Python issue2604: DocTestCase.tearDown clobbers globs
  517. def saveGlobals(test):
  518. """
  519. Save C{test.globs} and replace it with a copy so that if
  520. necessary, the original will be available for the next test
  521. run.
  522. """
  523. test._savedGlobals = getattr(test, "_savedGlobals", test.globs)
  524. test.globs = test._savedGlobals.copy()
  525. extraArgs["setUp"] = saveGlobals
  526. return doctest.DocTestSuite(module, **extraArgs)
  527. def loadAnything(self, obj, recurse=False, parent=None, qualName=None):
  528. """
  529. Load absolutely anything (as long as that anything is a module,
  530. package, class, or method (with associated parent class and qualname).
  531. @param obj: The object to load.
  532. @param recurse: A boolean. If True, inspect modules within packages
  533. within the given package (and so on), otherwise, only inspect
  534. modules in the package itself.
  535. @param parent: If C{obj} is a method, this is the parent class of the
  536. method. C{qualName} is also required.
  537. @param qualName: If C{obj} is a method, this a list containing is the
  538. qualified name of the method. C{parent} is also required.
  539. @return: A C{TestCase} or C{TestSuite}.
  540. """
  541. if isinstance(obj, types.ModuleType):
  542. # It looks like a module
  543. if isPackage(obj):
  544. # It's a package, so recurse down it.
  545. return self.loadPackage(obj, recurse=recurse)
  546. # Otherwise get all the tests in the module.
  547. return self.loadTestsFromModule(obj)
  548. elif isinstance(obj, type) and issubclass(obj, pyunit.TestCase):
  549. # We've found a raw test case, get the tests from it.
  550. return self.loadTestsFromTestCase(obj)
  551. elif (
  552. isinstance(obj, types.FunctionType)
  553. and isinstance(parent, type)
  554. and issubclass(parent, pyunit.TestCase)
  555. ):
  556. # We've found a method, and its parent is a TestCase. Instantiate
  557. # it with the name of the method we want.
  558. name = qualName[-1]
  559. inst = parent(name)
  560. # Sanity check to make sure that the method we have got from the
  561. # test case is the same one as was passed in. This doesn't actually
  562. # use the function we passed in, because reasons.
  563. assert getattr(inst, inst._testMethodName).__func__ == obj
  564. return inst
  565. elif isinstance(obj, TestSuite):
  566. # We've found a test suite.
  567. return obj
  568. else:
  569. raise TypeError(f"don't know how to make test from: {obj}")
  570. def loadByName(self, name, recurse=False):
  571. """
  572. Load some tests by name.
  573. @param name: The qualified name for the test to load.
  574. @param recurse: A boolean. If True, inspect modules within packages
  575. within the given package (and so on), otherwise, only inspect
  576. modules in the package itself.
  577. """
  578. try:
  579. return self.suiteFactory([self.findByName(name, recurse=recurse)])
  580. except BaseException:
  581. return self.suiteFactory([ErrorHolder(name, failure.Failure())])
  582. loadTestsFromName = loadByName
  583. def loadByNames(self, names: List[str], recurse: bool = False) -> TestSuite:
  584. """
  585. Load some tests by a list of names.
  586. @param names: A L{list} of qualified names.
  587. @param recurse: A boolean. If True, inspect modules within packages
  588. within the given package (and so on), otherwise, only inspect
  589. modules in the package itself.
  590. """
  591. things = []
  592. errors = []
  593. for name in names:
  594. try:
  595. things.append(self.loadByName(name, recurse=recurse))
  596. except BaseException:
  597. errors.append(ErrorHolder(name, failure.Failure()))
  598. things.extend(errors)
  599. return self.suiteFactory(self._uniqueTests(things))
  600. def _uniqueTests(self, things):
  601. """
  602. Gather unique suite objects from loaded things. This will guarantee
  603. uniqueness of inherited methods on TestCases which would otherwise hash
  604. to same value and collapse to one test unexpectedly if using simpler
  605. means: e.g. set().
  606. """
  607. seen = set()
  608. for testthing in things:
  609. testthings = testthing._tests
  610. for thing in testthings:
  611. # This is horrible.
  612. if str(thing) not in seen:
  613. yield thing
  614. seen.add(str(thing))
  615. def loadFile(self, fileName, recurse=False):
  616. """
  617. Load a file, and then the tests in that file.
  618. @param fileName: The file name to load.
  619. @param recurse: A boolean. If True, inspect modules within packages
  620. within the given package (and so on), otherwise, only inspect
  621. modules in the package itself.
  622. """
  623. name = reflect.filenameToModuleName(fileName)
  624. try:
  625. module = SourceFileLoader(name, fileName).load_module()
  626. return self.loadAnything(module, recurse=recurse)
  627. except OSError:
  628. raise ValueError(f"{fileName} is not a Python file.")
  629. def _qualNameWalker(qualName):
  630. """
  631. Given a Python qualified name, this function yields a 2-tuple of the most
  632. specific qualified name first, followed by the next-most-specific qualified
  633. name, and so on, paired with the remainder of the qualified name.
  634. @param qualName: A Python qualified name.
  635. @type qualName: L{str}
  636. """
  637. # Yield what we were just given
  638. yield (qualName, [])
  639. # If they want more, split the qualified name up
  640. qualParts = qualName.split(".")
  641. for index in range(1, len(qualParts)):
  642. # This code here will produce, from the example walker.texas.ranger:
  643. # (walker.texas, ["ranger"])
  644. # (walker, ["texas", "ranger"])
  645. yield (".".join(qualParts[:-index]), qualParts[-index:])
  646. @contextmanager
  647. def _testDirectory(workingDirectory: str) -> Generator[None, None, None]:
  648. """
  649. A context manager which obtains a lock on a trial working directory
  650. and enters (L{os.chdir}) it and then reverses these things.
  651. @param workingDirectory: A pattern for the basename of the working
  652. directory to acquire.
  653. """
  654. currentDir = os.getcwd()
  655. base = filepath.FilePath(workingDirectory)
  656. testdir, testDirLock = util._unusedTestDirectory(base)
  657. os.chdir(testdir.path)
  658. yield
  659. os.chdir(currentDir)
  660. testDirLock.unlock()
  661. @contextmanager
  662. def _logFile(logfile: str) -> Generator[None, None, None]:
  663. """
  664. A context manager which adds a log observer and then removes it.
  665. @param logfile: C{"-"} f or stdout logging, otherwise the path to a log
  666. file to which to write.
  667. """
  668. if logfile == "-":
  669. logFile = sys.stdout
  670. else:
  671. logFile = util.openTestLog(filepath.FilePath(logfile))
  672. logFileObserver = log.FileLogObserver(logFile)
  673. observerFunction = logFileObserver.emit
  674. log.startLoggingWithObserver(observerFunction, 0)
  675. yield
  676. log.removeObserver(observerFunction)
  677. logFile.close()
  678. class _Runner(Protocol):
  679. stream: TextIO
  680. def run(self, test: Union[pyunit.TestCase, pyunit.TestSuite]) -> itrial.IReporter:
  681. ...
  682. def runUntilFailure(
  683. self, test: Union[pyunit.TestCase, pyunit.TestSuite]
  684. ) -> itrial.IReporter:
  685. ...
  686. @define
  687. class TrialRunner:
  688. """
  689. A specialised runner that the trial front end uses.
  690. @ivar reporterFactory: A callable to create a reporter to use.
  691. @ivar mode: Either C{None} for a normal test run, L{TrialRunner.DEBUG} for
  692. a run in the debugger, or L{TrialRunner.DRY_RUN} to collect and report
  693. the tests but not call any of them.
  694. @ivar logfile: The path to the file to write the test run log.
  695. @ivar stream: The file to report results to.
  696. @ivar profile: C{True} to run the tests with a profiler enabled.
  697. @ivar _tracebackFormat: A format name to use with L{Failure} for reporting
  698. failures.
  699. @ivar _realTimeErrors: C{True} if errors should be reported as they
  700. happen. C{False} if they should only be reported at the end of the
  701. test run in the summary.
  702. @ivar uncleanWarnings: C{True} to report dirty reactor errors as warnings,
  703. C{False} to report them as test-failing errors.
  704. @ivar workingDirectory: A path template to a directory which will be the
  705. process's working directory while the tests are running.
  706. @ivar _forceGarbageCollection: C{True} to perform a full garbage
  707. collection at least after each test. C{False} to let garbage
  708. collection run only when it normally would.
  709. @ivar debugger: In debug mode, an object to use to launch the debugger.
  710. @ivar _exitFirst: C{True} to stop after the first failed test. C{False}
  711. to run the whole suite.
  712. @ivar log: An object to give to the reporter to use as a log publisher.
  713. """
  714. DEBUG = "debug"
  715. DRY_RUN = "dry-run"
  716. reporterFactory: Callable[[TextIO, str, bool, log.LogPublisher], itrial.IReporter]
  717. mode: Optional[str] = None
  718. logfile: str = "test.log"
  719. stream: TextIO = sys.stdout
  720. profile: bool = False
  721. _tracebackFormat: str = "default"
  722. _realTimeErrors: bool = False
  723. uncleanWarnings: bool = False
  724. workingDirectory: str = "_trial_temp"
  725. _forceGarbageCollection: bool = False
  726. debugger: Optional[_Debugger] = None
  727. _exitFirst: bool = False
  728. _log: log.LogPublisher = log # type: ignore[assignment]
  729. def _makeResult(self) -> itrial.IReporter:
  730. reporter = self.reporterFactory(
  731. self.stream, self.tbformat, self.rterrors, self._log
  732. )
  733. if self._exitFirst:
  734. reporter = _ExitWrapper(reporter)
  735. if self.uncleanWarnings:
  736. reporter = UncleanWarningsReporterWrapper(reporter)
  737. return reporter
  738. @property
  739. def tbformat(self) -> str:
  740. return self._tracebackFormat
  741. @property
  742. def rterrors(self) -> bool:
  743. return self._realTimeErrors
  744. def run(self, test: Union[pyunit.TestCase, pyunit.TestSuite]) -> itrial.IReporter:
  745. """
  746. Run the test or suite and return a result object.
  747. """
  748. test = unittest.decorate(test, ITestCase)
  749. if self.profile:
  750. run = util.profiled(self._runWithoutDecoration, "profile.data")
  751. else:
  752. run = self._runWithoutDecoration
  753. return run(test, self._forceGarbageCollection)
  754. def _runWithoutDecoration(
  755. self,
  756. test: Union[pyunit.TestCase, pyunit.TestSuite],
  757. forceGarbageCollection: bool = False,
  758. ) -> itrial.IReporter:
  759. """
  760. Private helper that runs the given test but doesn't decorate it.
  761. """
  762. result = self._makeResult()
  763. # decorate the suite with reactor cleanup and log starting
  764. # This should move out of the runner and be presumed to be
  765. # present
  766. suite = TrialSuite([test], forceGarbageCollection)
  767. if self.mode == self.DRY_RUN:
  768. for single in _iterateTests(suite):
  769. result.startTest(single)
  770. result.addSuccess(single)
  771. result.stopTest(single)
  772. else:
  773. if self.mode == self.DEBUG:
  774. assert self.debugger is not None
  775. run = lambda: self.debugger.runcall(suite.run, result)
  776. else:
  777. run = lambda: suite.run(result)
  778. with _testDirectory(self.workingDirectory), _logFile(self.logfile):
  779. run()
  780. result.done()
  781. return result
  782. def runUntilFailure(
  783. self, test: Union[pyunit.TestCase, pyunit.TestSuite]
  784. ) -> itrial.IReporter:
  785. """
  786. Repeatedly run C{test} until it fails.
  787. """
  788. count = 0
  789. while True:
  790. count += 1
  791. self.stream.write("Test Pass %d\n" % (count,))
  792. if count == 1:
  793. # If test is a TestSuite, run *mutates it*. So only follow
  794. # this code-path once! Otherwise the decorations accumulate
  795. # forever.
  796. result = self.run(test)
  797. else:
  798. result = self._runWithoutDecoration(test)
  799. if result.testsRun == 0:
  800. break
  801. if not result.wasSuccessful():
  802. break
  803. return result