trial.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. # -*- test-case-name: twisted.trial.test.test_script -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. import gc
  5. import inspect
  6. import os
  7. import pdb
  8. import random
  9. import sys
  10. import time
  11. import trace
  12. import warnings
  13. from typing import NoReturn, Optional, Type
  14. from twisted import plugin
  15. from twisted.application import app
  16. from twisted.internet import defer
  17. from twisted.python import failure, reflect, usage
  18. from twisted.python.filepath import FilePath
  19. from twisted.python.reflect import namedModule
  20. from twisted.trial import itrial, runner
  21. from twisted.trial._dist.disttrial import DistTrialRunner
  22. from twisted.trial.unittest import TestSuite
  23. # Yea, this is stupid. Leave it for command-line compatibility for a
  24. # while, though.
  25. TBFORMAT_MAP = {
  26. "plain": "default",
  27. "default": "default",
  28. "emacs": "brief",
  29. "brief": "brief",
  30. "cgitb": "verbose",
  31. "verbose": "verbose",
  32. }
  33. def _autoJobs() -> int:
  34. """
  35. Heuristically guess the number of job workers to run.
  36. When ``os.process_cpu_count()`` is available (Python 3.13+),
  37. return the number of logical CPUs usable by the current
  38. process. This respects the ``PYTHON_CPU_COUNT`` environment
  39. variable and/or ``python -X cpu_count`` flag.
  40. Otherwise, if ``os.sched_getaffinity()`` is available (on some
  41. Unixes) this returns the number of CPUs this process is
  42. restricted to, under the assumption that this affinity will
  43. be inherited.
  44. Otherwise, consult ``os.cpu_count()`` to get the number of
  45. logical CPUs.
  46. Failing all else, return 1.
  47. @returns: A strictly positive integer.
  48. """
  49. number: Optional[int]
  50. if getattr(os, "process_cpu_count", None) is not None:
  51. number = os.process_cpu_count() # type: ignore[attr-defined]
  52. elif getattr(os, "sched_getaffinity", None) is not None:
  53. number = len(os.sched_getaffinity(0))
  54. else:
  55. number = os.cpu_count()
  56. if number is None or number < 1:
  57. return 1
  58. return number
  59. def _parseLocalVariables(line):
  60. """
  61. Accepts a single line in Emacs local variable declaration format and
  62. returns a dict of all the variables {name: value}.
  63. Raises ValueError if 'line' is in the wrong format.
  64. See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
  65. """
  66. paren = "-*-"
  67. start = line.find(paren) + len(paren)
  68. end = line.rfind(paren)
  69. if start == -1 or end == -1:
  70. raise ValueError(f"{line!r} not a valid local variable declaration")
  71. items = line[start:end].split(";")
  72. localVars = {}
  73. for item in items:
  74. if len(item.strip()) == 0:
  75. continue
  76. split = item.split(":")
  77. if len(split) != 2:
  78. raise ValueError(f"{line!r} contains invalid declaration {item!r}")
  79. localVars[split[0].strip()] = split[1].strip()
  80. return localVars
  81. def loadLocalVariables(filename):
  82. """
  83. Accepts a filename and attempts to load the Emacs variable declarations
  84. from that file, simulating what Emacs does.
  85. See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
  86. """
  87. with open(filename) as f:
  88. lines = [f.readline(), f.readline()]
  89. for line in lines:
  90. try:
  91. return _parseLocalVariables(line)
  92. except ValueError:
  93. pass
  94. return {}
  95. def getTestModules(filename):
  96. testCaseVar = loadLocalVariables(filename).get("test-case-name", None)
  97. if testCaseVar is None:
  98. return []
  99. return testCaseVar.split(",")
  100. def isTestFile(filename):
  101. """
  102. Returns true if 'filename' looks like a file containing unit tests.
  103. False otherwise. Doesn't care whether filename exists.
  104. """
  105. basename = os.path.basename(filename)
  106. return basename.startswith("test_") and os.path.splitext(basename)[1] == (".py")
  107. def _reporterAction():
  108. return usage.CompleteList([p.longOpt for p in plugin.getPlugins(itrial.IReporter)])
  109. def _maybeFindSourceLine(testThing):
  110. """
  111. Try to find the source line of the given test thing.
  112. @param testThing: the test item to attempt to inspect
  113. @type testThing: an L{TestCase}, test method, or module, though only the
  114. former two have a chance to succeed
  115. @rtype: int
  116. @return: the starting source line, or -1 if one couldn't be found
  117. """
  118. # an instance of L{TestCase} -- locate the test it will run
  119. method = getattr(testThing, "_testMethodName", None)
  120. if method is not None:
  121. testThing = getattr(testThing, method)
  122. # If it's a function, we can get the line number even if the source file no
  123. # longer exists
  124. code = getattr(testThing, "__code__", None)
  125. if code is not None:
  126. return code.co_firstlineno
  127. try:
  128. return inspect.getsourcelines(testThing)[1]
  129. except (OSError, TypeError):
  130. # either testThing is a module, which raised a TypeError, or the file
  131. # couldn't be read
  132. return -1
  133. # orders which can be passed to trial --order
  134. _runOrders = {
  135. "alphabetical": (
  136. "alphabetical order for test methods, arbitrary order for test cases",
  137. runner.name,
  138. ),
  139. "toptobottom": (
  140. "attempt to run test cases and methods in the order they were defined",
  141. _maybeFindSourceLine,
  142. ),
  143. }
  144. def _checkKnownRunOrder(order):
  145. """
  146. Check that the given order is a known test running order.
  147. Does nothing else, since looking up the appropriate callable to sort the
  148. tests should be done when it actually will be used, as the default argument
  149. will not be coerced by this function.
  150. @param order: one of the known orders in C{_runOrders}
  151. @return: the order unmodified
  152. """
  153. if order not in _runOrders:
  154. raise usage.UsageError(
  155. "--order must be one of: %s. See --help-orders for details"
  156. % (", ".join(repr(order) for order in _runOrders),)
  157. )
  158. return order
  159. class _BasicOptions:
  160. """
  161. Basic options shared between trial and its local workers.
  162. """
  163. longdesc = (
  164. "trial loads and executes a suite of unit tests, obtained "
  165. "from modules, packages and files listed on the command line."
  166. )
  167. optFlags = [
  168. ["help", "h"],
  169. ["no-recurse", "N", "Don't recurse into packages"],
  170. ["help-orders", None, "Help on available test running orders"],
  171. ["help-reporters", None, "Help on available output plugins (reporters)"],
  172. [
  173. "rterrors",
  174. "e",
  175. "realtime errors, print out tracebacks as " "soon as they occur",
  176. ],
  177. ["unclean-warnings", None, "Turn dirty reactor errors into warnings"],
  178. [
  179. "force-gc",
  180. None,
  181. "Have Trial run gc.collect() before and " "after each test case.",
  182. ],
  183. [
  184. "exitfirst",
  185. "x",
  186. "Exit after the first non-successful result (cannot be "
  187. "specified along with --jobs).",
  188. ],
  189. ]
  190. optParameters = [
  191. [
  192. "order",
  193. "o",
  194. None,
  195. "Specify what order to run test cases and methods. "
  196. "See --help-orders for more info.",
  197. _checkKnownRunOrder,
  198. ],
  199. ["random", "z", None, "Run tests in random order using the specified seed"],
  200. [
  201. "temp-directory",
  202. None,
  203. "_trial_temp",
  204. "Path to use as working directory for tests.",
  205. ],
  206. [
  207. "reporter",
  208. None,
  209. "verbose",
  210. "The reporter to use for this test run. See --help-reporters for "
  211. "more info.",
  212. ],
  213. ]
  214. compData = usage.Completions(
  215. optActions={
  216. "order": usage.CompleteList(_runOrders),
  217. "reporter": _reporterAction,
  218. "logfile": usage.CompleteFiles(descr="log file name"),
  219. "random": usage.Completer(descr="random seed"),
  220. },
  221. extraActions=[
  222. usage.CompleteFiles(
  223. "*.py",
  224. descr="file | module | package | TestCase | testMethod",
  225. repeat=True,
  226. )
  227. ],
  228. )
  229. tracer: Optional[trace.Trace] = None
  230. def __init__(self):
  231. self["tests"] = []
  232. usage.Options.__init__(self)
  233. def getSynopsis(self):
  234. executableName = reflect.filenameToModuleName(sys.argv[0])
  235. if executableName.endswith(".__main__"):
  236. executableName = "{} -m {}".format(
  237. os.path.basename(sys.executable),
  238. executableName.replace(".__main__", ""),
  239. )
  240. return """{} [options] [[file|package|module|TestCase|testmethod]...]
  241. """.format(
  242. executableName,
  243. )
  244. def coverdir(self):
  245. """
  246. Return a L{FilePath} representing the directory into which coverage
  247. results should be written.
  248. """
  249. coverdir = "coverage"
  250. result = FilePath(self["temp-directory"]).child(coverdir)
  251. print(f"Setting coverage directory to {result.path}.")
  252. return result
  253. # TODO: Some of the opt_* methods on this class have docstrings and some do
  254. # not. This is mostly because usage.Options's currently will replace
  255. # any intended output in optFlags and optParameters with the
  256. # docstring. See #6427. When that is fixed, all methods should be
  257. # given docstrings (and it should be verified that those with
  258. # docstrings already have content suitable for printing as usage
  259. # information).
  260. def opt_coverage(self):
  261. """
  262. Generate coverage information in the coverage file in the
  263. directory specified by the temp-directory option.
  264. """
  265. self.tracer = trace.Trace(count=1, trace=0)
  266. sys.settrace(self.tracer.globaltrace)
  267. self["coverage"] = True
  268. def opt_testmodule(self, filename):
  269. """
  270. Filename to grep for test cases (-*- test-case-name).
  271. """
  272. # If the filename passed to this parameter looks like a test module
  273. # we just add that to the test suite.
  274. #
  275. # If not, we inspect it for an Emacs buffer local variable called
  276. # 'test-case-name'. If that variable is declared, we try to add its
  277. # value to the test suite as a module.
  278. #
  279. # This parameter allows automated processes (like Buildbot) to pass
  280. # a list of files to Trial with the general expectation of "these files,
  281. # whatever they are, will get tested"
  282. if not os.path.isfile(filename):
  283. sys.stderr.write(f"File {filename!r} doesn't exist\n")
  284. return
  285. filename = os.path.abspath(filename)
  286. if isTestFile(filename):
  287. self["tests"].append(filename)
  288. else:
  289. self["tests"].extend(getTestModules(filename))
  290. def opt_spew(self):
  291. """
  292. Print an insanely verbose log of everything that happens. Useful
  293. when debugging freezes or locks in complex code.
  294. """
  295. from twisted.python.util import spewer
  296. sys.settrace(spewer)
  297. def opt_help_orders(self):
  298. synopsis = (
  299. "Trial can attempt to run test cases and their methods in "
  300. "a few different orders. You can select any of the "
  301. "following options using --order=<foo>.\n"
  302. )
  303. print(synopsis)
  304. for name, (description, _) in sorted(_runOrders.items()):
  305. print(" ", name, "\t", description)
  306. sys.exit(0)
  307. def opt_help_reporters(self):
  308. synopsis = (
  309. "Trial's output can be customized using plugins called "
  310. "Reporters. You can\nselect any of the following "
  311. "reporters using --reporter=<foo>\n"
  312. )
  313. print(synopsis)
  314. for p in plugin.getPlugins(itrial.IReporter):
  315. print(" ", p.longOpt, "\t", p.description)
  316. sys.exit(0)
  317. def opt_disablegc(self):
  318. """
  319. Disable the garbage collector
  320. """
  321. self["disablegc"] = True
  322. gc.disable()
  323. def opt_tbformat(self, opt):
  324. """
  325. Specify the format to display tracebacks with. Valid formats are
  326. 'plain', 'emacs', and 'cgitb' which uses the nicely verbose stdlib
  327. cgitb.text function
  328. """
  329. try:
  330. self["tbformat"] = TBFORMAT_MAP[opt]
  331. except KeyError:
  332. raise usage.UsageError("tbformat must be 'plain', 'emacs', or 'cgitb'.")
  333. def opt_recursionlimit(self, arg):
  334. """
  335. see sys.setrecursionlimit()
  336. """
  337. try:
  338. sys.setrecursionlimit(int(arg))
  339. except (TypeError, ValueError):
  340. raise usage.UsageError("argument to recursionlimit must be an integer")
  341. else:
  342. self["recursionlimit"] = int(arg)
  343. def opt_random(self, option):
  344. try:
  345. self["random"] = int(option)
  346. except ValueError:
  347. raise usage.UsageError("Argument to --random must be a positive integer")
  348. else:
  349. if self["random"] < 0:
  350. raise usage.UsageError(
  351. "Argument to --random must be a positive integer"
  352. )
  353. elif self["random"] == 0:
  354. self["random"] = int(time.time() * 100)
  355. def opt_without_module(self, option):
  356. """
  357. Fake the lack of the specified modules, separated with commas.
  358. """
  359. self["without-module"] = option
  360. for module in option.split(","):
  361. if module in sys.modules:
  362. warnings.warn(
  363. "Module '%s' already imported, " "disabling anyway." % (module,),
  364. category=RuntimeWarning,
  365. )
  366. sys.modules[module] = None
  367. def parseArgs(self, *args):
  368. self["tests"].extend(args)
  369. def _loadReporterByName(self, name):
  370. for p in plugin.getPlugins(itrial.IReporter):
  371. qual = f"{p.module}.{p.klass}"
  372. if p.longOpt == name:
  373. return reflect.namedAny(qual)
  374. raise usage.UsageError(
  375. "Only pass names of Reporter plugins to "
  376. "--reporter. See --help-reporters for "
  377. "more info."
  378. )
  379. def postOptions(self):
  380. # Only load reporters now, as opposed to any earlier, to avoid letting
  381. # application-defined plugins muck up reactor selecting by importing
  382. # t.i.reactor and causing the default to be installed.
  383. self["reporter"] = self._loadReporterByName(self["reporter"])
  384. if "tbformat" not in self:
  385. self["tbformat"] = "default"
  386. if self["order"] is not None and self["random"] is not None:
  387. raise usage.UsageError("You can't specify --random when using --order")
  388. class Options(_BasicOptions, usage.Options, app.ReactorSelectionMixin):
  389. """
  390. Options to the trial command line tool.
  391. @ivar _workerFlags: List of flags which are accepted by trial distributed
  392. workers. This is used by C{_getWorkerArguments} to build the command
  393. line arguments.
  394. @type _workerFlags: C{list}
  395. @ivar _workerParameters: List of parameter which are accepted by trial
  396. distributed workers. This is used by C{_getWorkerArguments} to build
  397. the command line arguments.
  398. @type _workerParameters: C{list}
  399. """
  400. optFlags = [
  401. [
  402. "debug",
  403. "b",
  404. "Run tests in a debugger. If that debugger is "
  405. "pdb, will load '.pdbrc' from current directory if it exists.",
  406. ],
  407. [
  408. "debug-stacktraces",
  409. "B",
  410. "Report Deferred creation and " "callback stack traces",
  411. ],
  412. [
  413. "nopm",
  414. None,
  415. "don't automatically jump into debugger for " "postmorteming of exceptions",
  416. ],
  417. ["dry-run", "n", "do everything but run the tests"],
  418. ["profile", None, "Run tests under the Python profiler"],
  419. ["until-failure", "u", "Repeat test until it fails"],
  420. ]
  421. optParameters = [
  422. [
  423. "debugger",
  424. None,
  425. "pdb",
  426. "the fully qualified name of a debugger to " "use if --debug is passed",
  427. ],
  428. ["logfile", "l", "test.log", "log file name"],
  429. ["jobs", "j", None, "Number of local workers to run"],
  430. ]
  431. compData = usage.Completions(
  432. optActions={
  433. "tbformat": usage.CompleteList(["plain", "emacs", "cgitb"]),
  434. "reporter": _reporterAction,
  435. },
  436. )
  437. _workerFlags = ["disablegc", "force-gc", "coverage"]
  438. _workerParameters = ["recursionlimit", "reactor", "without-module"]
  439. def opt_jobs(self, number):
  440. """
  441. Number of local workers to run, a strictly positive integer or 'auto'
  442. to spawn one worker for each available CPU.
  443. """
  444. if number == "auto":
  445. number = _autoJobs()
  446. else:
  447. try:
  448. number = int(number)
  449. except ValueError:
  450. raise usage.UsageError(
  451. "Expecting integer argument to jobs, got '%s'" % number
  452. )
  453. if number <= 0:
  454. raise usage.UsageError(
  455. "Argument to jobs must be a strictly positive integer or 'auto'"
  456. )
  457. self["jobs"] = number
  458. def _getWorkerArguments(self):
  459. """
  460. Return a list of options to pass to distributed workers.
  461. """
  462. args = []
  463. for option in self._workerFlags:
  464. if self.get(option) is not None:
  465. if self[option]:
  466. args.append(f"--{option}")
  467. for option in self._workerParameters:
  468. if self.get(option) is not None:
  469. args.extend([f"--{option}", str(self[option])])
  470. return args
  471. def postOptions(self):
  472. _BasicOptions.postOptions(self)
  473. if self["jobs"]:
  474. conflicts = ["debug", "profile", "debug-stacktraces"]
  475. for option in conflicts:
  476. if self[option]:
  477. raise usage.UsageError(
  478. "You can't specify --%s when using --jobs" % option
  479. )
  480. if self["nopm"]:
  481. if not self["debug"]:
  482. raise usage.UsageError("You must specify --debug when using " "--nopm ")
  483. failure.DO_POST_MORTEM = False
  484. def _initialDebugSetup(config: Options) -> None:
  485. # do this part of debug setup first for easy debugging of import failures
  486. if config["debug"]:
  487. failure.startDebugMode()
  488. if config["debug"] or config["debug-stacktraces"]:
  489. defer.setDebugging(True)
  490. def _getSuite(config: Options) -> TestSuite:
  491. loader = _getLoader(config)
  492. recurse = not config["no-recurse"]
  493. return loader.loadByNames(config["tests"], recurse=recurse)
  494. def _getLoader(config: Options) -> runner.TestLoader:
  495. loader = runner.TestLoader()
  496. if config["random"]:
  497. randomer = random.Random()
  498. randomer.seed(config["random"])
  499. loader.sorter = lambda x: randomer.random()
  500. print("Running tests shuffled with seed %d\n" % config["random"])
  501. elif config["order"]:
  502. _, sorter = _runOrders[config["order"]]
  503. loader.sorter = sorter
  504. if not config["until-failure"]:
  505. loader.suiteFactory = runner.DestructiveTestSuite
  506. return loader
  507. def _wrappedPdb():
  508. """
  509. Wrap an instance of C{pdb.Pdb} with readline support and load any .rcs.
  510. """
  511. dbg = pdb.Pdb()
  512. try:
  513. namedModule("readline")
  514. except ImportError:
  515. print("readline module not available")
  516. for path in (".pdbrc", "pdbrc"):
  517. if os.path.exists(path):
  518. try:
  519. rcFile = open(path)
  520. except OSError:
  521. pass
  522. else:
  523. with rcFile:
  524. dbg.rcLines.extend(rcFile.readlines())
  525. return dbg
  526. class _DebuggerNotFound(Exception):
  527. """
  528. A debugger import failed.
  529. Used to allow translating these errors into usage error messages.
  530. """
  531. def _makeRunner(config: Options) -> runner._Runner:
  532. """
  533. Return a trial runner class set up with the parameters extracted from
  534. C{config}.
  535. @return: A trial runner instance.
  536. """
  537. cls: Type[runner._Runner] = runner.TrialRunner
  538. args = {
  539. "reporterFactory": config["reporter"],
  540. "tracebackFormat": config["tbformat"],
  541. "realTimeErrors": config["rterrors"],
  542. "uncleanWarnings": config["unclean-warnings"],
  543. "logfile": config["logfile"],
  544. "workingDirectory": config["temp-directory"],
  545. "exitFirst": config["exitfirst"],
  546. }
  547. if config["dry-run"]:
  548. args["mode"] = runner.TrialRunner.DRY_RUN
  549. elif config["jobs"]:
  550. cls = DistTrialRunner
  551. args["maxWorkers"] = config["jobs"]
  552. args["workerArguments"] = config._getWorkerArguments()
  553. else:
  554. if config["debug"]:
  555. args["mode"] = runner.TrialRunner.DEBUG
  556. debugger = config["debugger"]
  557. if debugger != "pdb":
  558. try:
  559. args["debugger"] = reflect.namedAny(debugger)
  560. except reflect.ModuleNotFound:
  561. raise _DebuggerNotFound(
  562. f"{debugger!r} debugger could not be found."
  563. )
  564. else:
  565. args["debugger"] = _wrappedPdb()
  566. args["profile"] = config["profile"]
  567. args["forceGarbageCollection"] = config["force-gc"]
  568. return cls(**args)
  569. def run() -> NoReturn:
  570. if len(sys.argv) == 1:
  571. sys.argv.append("--help")
  572. config = Options()
  573. try:
  574. config.parseOptions()
  575. except usage.error as ue:
  576. raise SystemExit(f"{sys.argv[0]}: {ue}")
  577. _initialDebugSetup(config)
  578. try:
  579. trialRunner = _makeRunner(config)
  580. except _DebuggerNotFound as e:
  581. raise SystemExit(f"{sys.argv[0]}: {str(e)}")
  582. suite = _getSuite(config)
  583. if config["until-failure"]:
  584. testResult = trialRunner.runUntilFailure(suite)
  585. else:
  586. testResult = trialRunner.run(suite)
  587. if config.tracer:
  588. sys.settrace(None)
  589. results = config.tracer.results()
  590. results.write_results(
  591. show_missing=True, summary=False, coverdir=config.coverdir().path
  592. )
  593. sys.exit(not testResult.wasSuccessful())