1
0

__init__.py 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243
  1. # -*- coding: utf-8 -*-
  2. """
  3. tests.support.parser
  4. ~~~~~~~~~~~~~~~~~~~~
  5. Salt Tests CLI access classes
  6. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  7. :copyright: Copyright 2013-2017 by the SaltStack Team, see AUTHORS for more details
  8. :license: Apache 2.0, see LICENSE for more details.
  9. """
  10. # pylint: disable=repr-flag-used-in-string
  11. from __future__ import absolute_import, print_function
  12. import fnmatch
  13. import logging
  14. import optparse
  15. import os
  16. import platform
  17. import re
  18. import shutil
  19. import signal
  20. import subprocess
  21. import sys
  22. import tempfile
  23. import time
  24. import traceback
  25. import warnings
  26. from collections import namedtuple
  27. from functools import partial
  28. import salt.utils.data
  29. import salt.utils.files
  30. import salt.utils.path
  31. import salt.utils.platform
  32. import salt.utils.stringutils
  33. import salt.utils.yaml
  34. import tests.support.paths
  35. # Import 3rd-party libs
  36. from salt.ext import six
  37. from tests.support import processes
  38. from tests.support.unit import TestLoader, TextTestRunner
  39. from tests.support.xmlunit import HAS_XMLRUNNER, XMLTestRunner
  40. try:
  41. from tests.support.ext import console
  42. WIDTH, HEIGHT = console.getTerminalSize()
  43. PNUM = WIDTH
  44. except Exception: # pylint: disable=broad-except
  45. PNUM = 70
  46. log = logging.getLogger(__name__)
  47. # This is a completely random and meaningful number intended to identify our
  48. # own signal triggering.
  49. WEIRD_SIGNAL_NUM = -45654
  50. def __global_logging_exception_handler(
  51. exc_type,
  52. exc_value,
  53. exc_traceback,
  54. _logger=logging.getLogger(__name__),
  55. _stderr=sys.__stderr__,
  56. _format_exception=traceback.format_exception,
  57. ):
  58. """
  59. This function will log all python exceptions.
  60. """
  61. if exc_type.__name__ == "KeyboardInterrupt":
  62. # Call the original sys.excepthook
  63. sys.__excepthook__(exc_type, exc_value, exc_traceback)
  64. return
  65. # Log the exception
  66. try:
  67. msg = "An un-handled exception was caught by salt's testing global exception handler:\n{}: {}\n{}".format(
  68. exc_type.__name__,
  69. exc_value,
  70. "".join(_format_exception(exc_type, exc_value, exc_traceback)).strip(),
  71. )
  72. except Exception: # pylint: disable=broad-except
  73. msg = (
  74. "An un-handled exception was caught by salt-testing's global exception handler:\n{}: {}\n"
  75. "(UNABLE TO FORMAT TRACEBACK)".format(exc_type.__name__, exc_value,)
  76. )
  77. try:
  78. _logger(__name__).error(msg)
  79. except Exception: # pylint: disable=broad-except
  80. # Python is shutting down and logging has been set to None already
  81. try:
  82. _stderr.write(msg + "\n")
  83. except Exception: # pylint: disable=broad-except
  84. # We have also lost reference to sys.__stderr__ ?!
  85. print(msg)
  86. # Call the original sys.excepthook
  87. try:
  88. sys.__excepthook__(exc_type, exc_value, exc_traceback)
  89. except Exception: # pylint: disable=broad-except
  90. # Python is shutting down and sys has been set to None already
  91. pass
  92. # Set our own exception handler as the one to use
  93. sys.excepthook = __global_logging_exception_handler
  94. TestsuiteResult = namedtuple(
  95. "TestsuiteResult", ["header", "errors", "skipped", "failures", "passed"]
  96. )
  97. TestResult = namedtuple("TestResult", ["id", "reason"])
  98. def print_header(
  99. header, sep="~", top=True, bottom=True, inline=False, centered=False, width=PNUM
  100. ):
  101. """
  102. Allows some pretty printing of headers on the console, either with a
  103. "ruler" on bottom and/or top, inline, centered, etc.
  104. """
  105. if top and not inline:
  106. print(sep * width)
  107. if centered and not inline:
  108. fmt = u"{0:^{width}}"
  109. elif inline and not centered:
  110. fmt = u"{0:{sep}<{width}}"
  111. elif inline and centered:
  112. fmt = u"{0:{sep}^{width}}"
  113. else:
  114. fmt = u"{0}"
  115. print(fmt.format(header, sep=sep, width=width))
  116. if bottom and not inline:
  117. print(sep * width)
  118. class SaltTestingParser(optparse.OptionParser):
  119. support_docker_execution = False
  120. support_destructive_tests_selection = False
  121. support_expensive_tests_selection = False
  122. source_code_basedir = None
  123. _known_interpreters = {
  124. "salttest/arch": "python2",
  125. "salttest/centos-5": "python2.6",
  126. "salttest/centos-6": "python2.6",
  127. "salttest/debian-7": "python2.7",
  128. "salttest/opensuse-12.3": "python2.7",
  129. "salttest/ubuntu-12.04": "python2.7",
  130. "salttest/ubuntu-12.10": "python2.7",
  131. "salttest/ubuntu-13.04": "python2.7",
  132. "salttest/ubuntu-13.10": "python2.7",
  133. "salttest/py3": "python3",
  134. }
  135. def __init__(self, testsuite_directory, *args, **kwargs):
  136. if (
  137. kwargs.pop("html_output_from_env", None) is not None
  138. or kwargs.pop("html_output_dir", None) is not None
  139. ):
  140. warnings.warn(
  141. "The unit tests HTML support was removed from {0}. Please "
  142. "stop passing 'html_output_dir' or 'html_output_from_env' "
  143. "as arguments to {0}".format(self.__class__.__name__),
  144. category=DeprecationWarning,
  145. stacklevel=2,
  146. )
  147. # Get XML output settings
  148. xml_output_dir_env_var = kwargs.pop(
  149. "xml_output_from_env", "XML_TESTS_OUTPUT_DIR"
  150. )
  151. xml_output_dir = kwargs.pop("xml_output_dir", None)
  152. if xml_output_dir_env_var in os.environ:
  153. xml_output_dir = os.environ.get(xml_output_dir_env_var)
  154. if not xml_output_dir:
  155. xml_output_dir = os.path.join(
  156. tempfile.gettempdir() if platform.system() != "Darwin" else "/tmp",
  157. "xml-tests-output",
  158. )
  159. self.xml_output_dir = xml_output_dir
  160. # Get the desired logfile to use while running tests
  161. self.tests_logfile = kwargs.pop("tests_logfile", None)
  162. optparse.OptionParser.__init__(self, *args, **kwargs)
  163. self.testsuite_directory = testsuite_directory
  164. self.testsuite_results = []
  165. self.test_selection_group = optparse.OptionGroup(
  166. self, "Tests Selection Options", "Select which tests are to be executed"
  167. )
  168. if self.support_destructive_tests_selection is True:
  169. self.test_selection_group.add_option(
  170. "--run-destructive",
  171. action="store_true",
  172. default=False,
  173. help=(
  174. "Run destructive tests. These tests can include adding "
  175. "or removing users from your system for example. "
  176. "Default: %default"
  177. ),
  178. )
  179. if self.support_expensive_tests_selection is True:
  180. self.test_selection_group.add_option(
  181. "--run-expensive",
  182. action="store_true",
  183. default=False,
  184. help=(
  185. "Run expensive tests. Expensive tests are any tests that, "
  186. "once configured, cost money to run, such as creating or "
  187. "destroying cloud instances on a cloud provider."
  188. ),
  189. )
  190. self.test_selection_group.add_option(
  191. "--run-slow", action="store_true", default=False, help=("Run slow tests."),
  192. )
  193. self.test_selection_group.add_option(
  194. "-n",
  195. "--name",
  196. dest="name",
  197. action="append",
  198. default=[],
  199. help=(
  200. "Specific test name to run. A named test is the module path "
  201. "relative to the tests directory"
  202. ),
  203. )
  204. self.test_selection_group.add_option(
  205. "--names-file",
  206. dest="names_file",
  207. default=None,
  208. help=("The location of a newline delimited file of test names to " "run"),
  209. )
  210. self.test_selection_group.add_option(
  211. "--from-filenames",
  212. dest="from_filenames",
  213. action="append",
  214. default=None,
  215. help=(
  216. "Pass a comma-separated list of file paths, and any "
  217. "unit/integration test module which corresponds to the "
  218. "specified file(s) will be run. For example, a path of "
  219. "salt/modules/git.py would result in unit.modules.test_git "
  220. "and integration.modules.test_git being run. Absolute paths "
  221. "are assumed to be files containing relative paths, one per "
  222. "line. Providing the paths in a file can help get around "
  223. "shell character limits when the list of files is long."
  224. ),
  225. )
  226. self.test_selection_group.add_option(
  227. "--filename-map",
  228. dest="filename_map",
  229. default=None,
  230. help=(
  231. "Path to a YAML file mapping paths/path globs to a list "
  232. "of test names to run. See tests/filename_map.yml "
  233. "for example usage (when --from-filenames is used, this "
  234. "map file will be the default one used)."
  235. ),
  236. )
  237. self.add_option_group(self.test_selection_group)
  238. if self.support_docker_execution is True:
  239. self.docked_selection_group = optparse.OptionGroup(
  240. self,
  241. "Docked Tests Execution",
  242. "Run the tests suite under a Docker container. This allows, "
  243. "for example, to run destructive tests on your machine "
  244. "without actually breaking it in any way.",
  245. )
  246. self.docked_selection_group.add_option(
  247. "--docked",
  248. default=None,
  249. metavar="CONTAINER",
  250. help="Run the tests suite in the chosen Docker container",
  251. )
  252. self.docked_selection_group.add_option(
  253. "--docked-interpreter",
  254. default=None,
  255. metavar="PYTHON_INTERPRETER",
  256. help="The python binary name to use when calling the tests " "suite.",
  257. )
  258. self.docked_selection_group.add_option(
  259. "--docked-skip-delete",
  260. default=False,
  261. action="store_true",
  262. help="Skip docker container deletion on exit. Default: False",
  263. )
  264. self.docked_selection_group.add_option(
  265. "--docked-skip-delete-on-errors",
  266. default=False,
  267. action="store_true",
  268. help="Skip docker container deletion on exit if errors "
  269. "occurred. Default: False",
  270. )
  271. self.docked_selection_group.add_option(
  272. "--docker-binary",
  273. help="The docker binary on the host system. Default: %default",
  274. default="/usr/bin/docker",
  275. )
  276. self.add_option_group(self.docked_selection_group)
  277. self.output_options_group = optparse.OptionGroup(self, "Output Options")
  278. self.output_options_group.add_option(
  279. "-F",
  280. "--fail-fast",
  281. dest="failfast",
  282. default=False,
  283. action="store_true",
  284. help="Stop on first failure",
  285. )
  286. self.output_options_group.add_option(
  287. "-v",
  288. "--verbose",
  289. dest="verbosity",
  290. default=1,
  291. action="count",
  292. help="Verbose test runner output",
  293. )
  294. self.output_options_group.add_option(
  295. "--output-columns",
  296. default=PNUM,
  297. type=int,
  298. help="Number of maximum columns to use on the output",
  299. )
  300. self.output_options_group.add_option(
  301. "--tests-logfile",
  302. default=self.tests_logfile,
  303. help="The path to the tests suite logging logfile",
  304. )
  305. if self.xml_output_dir is not None:
  306. self.output_options_group.add_option(
  307. "-x",
  308. "--xml",
  309. "--xml-out",
  310. dest="xml_out",
  311. default=False,
  312. help="XML test runner output(Output directory: {0})".format(
  313. self.xml_output_dir
  314. ),
  315. )
  316. self.output_options_group.add_option(
  317. "--no-report",
  318. default=False,
  319. action="store_true",
  320. help="Do NOT show the overall tests result",
  321. )
  322. self.add_option_group(self.output_options_group)
  323. self.fs_cleanup_options_group = optparse.OptionGroup(
  324. self, "File system cleanup Options"
  325. )
  326. self.fs_cleanup_options_group.add_option(
  327. "--clean",
  328. dest="clean",
  329. default=True,
  330. action="store_true",
  331. help=(
  332. "Clean up test environment before and after running the "
  333. "tests suite (default behaviour)"
  334. ),
  335. )
  336. self.fs_cleanup_options_group.add_option(
  337. "--no-clean",
  338. dest="clean",
  339. action="store_false",
  340. help=(
  341. "Don't clean up test environment before and after the "
  342. "tests suite execution (speed up test process)"
  343. ),
  344. )
  345. self.add_option_group(self.fs_cleanup_options_group)
  346. self.setup_additional_options()
  347. @staticmethod
  348. def _expand_paths(paths):
  349. """
  350. Expand any comma-separated lists of paths, and return a set of all
  351. paths to ensure there are no duplicates.
  352. """
  353. ret = set()
  354. for path in paths:
  355. for item in [x.strip() for x in path.split(",")]:
  356. if not item:
  357. continue
  358. elif os.path.isabs(item):
  359. try:
  360. with salt.utils.files.fopen(item, "rb") as fp_:
  361. for line in fp_:
  362. line = salt.utils.stringutils.to_unicode(line.strip())
  363. if os.path.isabs(line):
  364. log.warning(
  365. "Invalid absolute path %s in %s, " "ignoring",
  366. line,
  367. item,
  368. )
  369. else:
  370. ret.add(line)
  371. except (IOError, OSError) as exc:
  372. log.error("Failed to read from %s: %s", item, exc)
  373. else:
  374. ret.add(item)
  375. return ret
  376. @property
  377. def _test_mods(self):
  378. """
  379. Use the test_mods generator to get all of the test module names, and
  380. then store them in a set so that further references to this attribute
  381. will not need to re-walk the test dir.
  382. """
  383. try:
  384. return self.__test_mods
  385. except AttributeError:
  386. self.__test_mods = set(tests.support.paths.list_test_mods())
  387. return self.__test_mods
  388. def _map_files(self, files):
  389. """
  390. Map the passed paths to test modules, returning a set of the mapped
  391. module names.
  392. """
  393. ret = set()
  394. if self.options.filename_map is not None:
  395. try:
  396. with salt.utils.files.fopen(self.options.filename_map) as fp_:
  397. filename_map = salt.utils.yaml.safe_load(fp_)
  398. except Exception as exc: # pylint: disable=broad-except
  399. raise RuntimeError("Failed to load filename map: {0}".format(exc))
  400. else:
  401. filename_map = {}
  402. def _add(comps):
  403. """
  404. Helper to add unit and integration tests matching a given mod path
  405. """
  406. mod_relname = ".".join(comps)
  407. ret.update(
  408. x
  409. for x in [
  410. ".".join(("unit", mod_relname)),
  411. ".".join(("integration", mod_relname)),
  412. ".".join(("multimaster", mod_relname)),
  413. ]
  414. if x in self._test_mods
  415. )
  416. # First, try a path match
  417. for path in files:
  418. match = re.match(
  419. r"^(salt/|tests/(unit|integration|multimaster)/)(.+\.py)$", path
  420. )
  421. if match:
  422. comps = match.group(3).split("/")
  423. # Find matches for a source file
  424. if match.group(1) == "salt/":
  425. if comps[-1] == "__init__.py":
  426. if len(comps) > 1:
  427. comps.pop(-1)
  428. comps[-1] = "test_" + comps[-1]
  429. else:
  430. comps[-1] = "test_{0}".format(comps[-1][:-3])
  431. # Direct name matches
  432. _add(comps)
  433. # State matches for execution modules of the same name
  434. # (e.g. unit.states.test_archive if
  435. # unit.modules.test_archive is being run)
  436. try:
  437. if comps[-2] == "modules":
  438. comps[-2] = "states"
  439. _add(comps)
  440. except IndexError:
  441. # Not an execution module. This is either directly in
  442. # the salt/ directory, or salt/something/__init__.py
  443. pass
  444. # Make sure to run a test module if it's been modified
  445. elif match.group(1).startswith("tests/"):
  446. comps.insert(0, match.group(2))
  447. if fnmatch.fnmatch(comps[-1], "test_*.py"):
  448. comps[-1] = comps[-1][:-3]
  449. test_name = ".".join(comps)
  450. if test_name in self._test_mods:
  451. ret.add(test_name)
  452. # Next, try the filename_map
  453. for path_expr in filename_map:
  454. for filename in files:
  455. if salt.utils.stringutils.expr_match(filename, path_expr):
  456. ret.update(filename_map[path_expr])
  457. break
  458. if any(x.startswith("integration.proxy.") for x in ret):
  459. # Ensure that the salt-proxy daemon is started for these tests.
  460. self.options.proxy = True
  461. if any(x.startswith("integration.ssh.") for x in ret):
  462. # Ensure that an ssh daemon is started for these tests.
  463. self.options.ssh = True
  464. return ret
  465. def parse_args(self, args=None, values=None):
  466. self.options, self.args = optparse.OptionParser.parse_args(self, args, values)
  467. file_names = []
  468. if self.options.names_file:
  469. # pylint: disable=resource-leakage
  470. with open(self.options.names_file, "rb") as fp_:
  471. for line in fp_.readlines():
  472. if six.PY2:
  473. file_names.append(line.strip())
  474. else:
  475. file_names.append(line.decode(__salt_system_encoding__).strip())
  476. # pylint: enable=resource-leakage
  477. if self.args:
  478. for fpath in self.args:
  479. if (
  480. os.path.isfile(fpath)
  481. and fpath.endswith(".py")
  482. and os.path.basename(fpath).startswith("test_")
  483. ):
  484. if fpath in file_names:
  485. self.options.name.append(fpath)
  486. continue
  487. self.exit(
  488. status=1, msg="'{}' is not a valid test module\n".format(fpath)
  489. )
  490. if self.options.from_filenames is not None:
  491. self.options.from_filenames = self._expand_paths(
  492. self.options.from_filenames
  493. )
  494. # Locate the default map file if one was not passed
  495. if self.options.filename_map is None:
  496. self.options.filename_map = salt.utils.path.join(
  497. tests.support.paths.TESTS_DIR, "filename_map.yml"
  498. )
  499. self.options.name.extend(self._map_files(self.options.from_filenames))
  500. if self.options.name and file_names:
  501. self.options.name = list(set(self.options.name).intersection(file_names))
  502. elif file_names:
  503. self.options.name = file_names
  504. print_header(u"", inline=True, width=self.options.output_columns)
  505. self.pre_execution_cleanup()
  506. if self.support_docker_execution and self.options.docked is not None:
  507. if self.source_code_basedir is None:
  508. raise RuntimeError(
  509. "You need to define the 'source_code_basedir' attribute "
  510. "in '{0}'.".format(self.__class__.__name__)
  511. )
  512. if "/" not in self.options.docked:
  513. self.options.docked = "salttest/{0}".format(self.options.docked)
  514. if self.options.docked_interpreter is None:
  515. self.options.docked_interpreter = self._known_interpreters.get(
  516. self.options.docked, "python"
  517. )
  518. # No more processing should be done. We'll exit with the return
  519. # code we get from the docker container execution
  520. self.exit(self.run_suite_in_docker())
  521. # Validate options after checking that we're not goint to execute the
  522. # tests suite under a docker container
  523. self._validate_options()
  524. print(" * Current Directory: {0}".format(os.getcwd()))
  525. print(" * Test suite is running under PID {0}".format(os.getpid()))
  526. self._setup_logging()
  527. try:
  528. return (self.options, self.args)
  529. finally:
  530. print_header(u"", inline=True, width=self.options.output_columns)
  531. def setup_additional_options(self):
  532. """
  533. Subclasses should add additional options in this overridden method
  534. """
  535. def _validate_options(self):
  536. """
  537. Validate the default available options
  538. """
  539. if (
  540. self.xml_output_dir is not None
  541. and self.options.xml_out
  542. and HAS_XMLRUNNER is False
  543. ):
  544. self.error(
  545. "'--xml' is not available. The xmlrunner library is not " "installed."
  546. )
  547. if self.options.xml_out:
  548. # Override any environment setting with the passed value
  549. self.xml_output_dir = self.options.xml_out
  550. if self.xml_output_dir is not None and self.options.xml_out:
  551. if not os.path.isdir(self.xml_output_dir):
  552. os.makedirs(self.xml_output_dir)
  553. os.environ["TESTS_XML_OUTPUT_DIR"] = self.xml_output_dir
  554. print(
  555. " * Generated unit test XML reports will be stored "
  556. "at {0!r}".format(self.xml_output_dir)
  557. )
  558. self.validate_options()
  559. if self.support_destructive_tests_selection and not os.environ.get(
  560. "DESTRUCTIVE_TESTS", None
  561. ):
  562. # Set the required environment variable in order to know if
  563. # destructive tests should be executed or not.
  564. os.environ["DESTRUCTIVE_TESTS"] = str(self.options.run_destructive)
  565. if self.support_expensive_tests_selection and not os.environ.get(
  566. "EXPENSIVE_TESTS", None
  567. ):
  568. # Set the required environment variable in order to know if
  569. # expensive tests should be executed or not.
  570. os.environ["EXPENSIVE_TESTS"] = str(self.options.run_expensive)
  571. if not os.environ.get("SLOW_TESTS", None):
  572. os.environ["SLOW_TESTS"] = str(self.options.run_slow)
  573. def validate_options(self):
  574. """
  575. Validate the provided options. Override this method to run your own
  576. validation procedures.
  577. """
  578. def _setup_logging(self):
  579. """
  580. Setup python's logging system to work with/for the tests suite
  581. """
  582. # Setup tests logging
  583. formatter = logging.Formatter(
  584. "%(asctime)s,%(msecs)03.0f [%(name)-5s:%(lineno)-4d]"
  585. "[%(levelname)-8s] %(message)s",
  586. datefmt="%H:%M:%S",
  587. )
  588. if not hasattr(logging, "TRACE"):
  589. logging.TRACE = 5
  590. logging.addLevelName(logging.TRACE, "TRACE")
  591. if not hasattr(logging, "GARBAGE"):
  592. logging.GARBAGE = 1
  593. logging.addLevelName(logging.GARBAGE, "GARBAGE")
  594. # Default logging level: ERROR
  595. logging.root.setLevel(logging.NOTSET)
  596. log_levels_to_evaluate = [
  597. logging.ERROR, # Default log level
  598. ]
  599. if self.options.tests_logfile:
  600. filehandler = logging.FileHandler(
  601. mode="w", # Not preserved between re-runs
  602. filename=self.options.tests_logfile,
  603. encoding="utf-8",
  604. )
  605. # The logs of the file are the most verbose possible
  606. filehandler.setLevel(logging.DEBUG)
  607. filehandler.setFormatter(formatter)
  608. logging.root.addHandler(filehandler)
  609. log_levels_to_evaluate.append(logging.DEBUG)
  610. print(" * Logging tests on {0}".format(self.options.tests_logfile))
  611. # With greater verbosity we can also log to the console
  612. if self.options.verbosity >= 2:
  613. consolehandler = logging.StreamHandler(sys.stderr)
  614. consolehandler.setFormatter(formatter)
  615. if self.options.verbosity >= 6: # -vvvvv
  616. logging_level = logging.GARBAGE
  617. elif self.options.verbosity == 5: # -vvvv
  618. logging_level = logging.TRACE
  619. elif self.options.verbosity == 4: # -vvv
  620. logging_level = logging.DEBUG
  621. elif self.options.verbosity == 3: # -vv
  622. logging_level = logging.INFO
  623. else:
  624. logging_level = logging.ERROR
  625. log_levels_to_evaluate.append(logging_level)
  626. os.environ["TESTS_LOG_LEVEL"] = str(
  627. self.options.verbosity
  628. ) # future lint: disable=blacklisted-function
  629. consolehandler.setLevel(logging_level)
  630. logging.root.addHandler(consolehandler)
  631. log.info("Runtests logging has been setup")
  632. os.environ["TESTS_MIN_LOG_LEVEL_NAME"] = logging.getLevelName(
  633. min(log_levels_to_evaluate)
  634. )
  635. def pre_execution_cleanup(self):
  636. """
  637. Run any initial clean up operations. If sub-classed, don't forget to
  638. call SaltTestingParser.pre_execution_cleanup(self) from the overridden
  639. method.
  640. """
  641. if self.options.clean is True:
  642. for path in (self.xml_output_dir,):
  643. if path is None:
  644. continue
  645. if os.path.isdir(path):
  646. shutil.rmtree(path)
  647. def run_suite(
  648. self,
  649. path,
  650. display_name,
  651. suffix="test_*.py",
  652. load_from_name=False,
  653. additional_test_dirs=None,
  654. failfast=False,
  655. ):
  656. """
  657. Execute a unit test suite
  658. """
  659. loaded_custom = False
  660. loader = TestLoader()
  661. try:
  662. if load_from_name:
  663. tests = loader.loadTestsFromName(display_name)
  664. else:
  665. if additional_test_dirs is None or self.testsuite_directory.startswith(
  666. path
  667. ):
  668. tests = loader.discover(path, suffix, self.testsuite_directory)
  669. else:
  670. tests = loader.discover(path, suffix)
  671. loaded_custom = True
  672. except (AttributeError, ImportError):
  673. print("Could not locate test '{0}'. Exiting.".format(display_name))
  674. sys.exit(1)
  675. if additional_test_dirs and not loaded_custom:
  676. for test_dir in additional_test_dirs:
  677. additional_tests = loader.discover(test_dir, suffix, test_dir)
  678. tests.addTests(additional_tests)
  679. header = "{0} Tests".format(display_name)
  680. print_header("Starting {0}".format(header), width=self.options.output_columns)
  681. if self.options.xml_out:
  682. runner = XMLTestRunner(
  683. stream=sys.stdout,
  684. output=self.xml_output_dir,
  685. verbosity=self.options.verbosity,
  686. failfast=failfast,
  687. ).run(tests)
  688. else:
  689. runner = TextTestRunner(
  690. stream=sys.stdout, verbosity=self.options.verbosity, failfast=failfast
  691. ).run(tests)
  692. errors = []
  693. skipped = []
  694. failures = []
  695. for testcase, reason in runner.errors:
  696. errors.append(TestResult(testcase.id(), reason))
  697. for testcase, reason in runner.skipped:
  698. skipped.append(TestResult(testcase.id(), reason))
  699. for testcase, reason in runner.failures:
  700. failures.append(TestResult(testcase.id(), reason))
  701. self.testsuite_results.append(
  702. TestsuiteResult(
  703. header,
  704. errors,
  705. skipped,
  706. failures,
  707. runner.testsRun - len(errors + skipped + failures),
  708. )
  709. )
  710. success = runner.wasSuccessful()
  711. del loader
  712. del runner
  713. return success
  714. def print_overall_testsuite_report(self):
  715. """
  716. Print a nicely formatted report about the test suite results
  717. """
  718. print()
  719. print_header(
  720. u" Overall Tests Report ",
  721. sep=u"=",
  722. centered=True,
  723. inline=True,
  724. width=self.options.output_columns,
  725. )
  726. failures = errors = skipped = passed = 0
  727. no_problems_found = True
  728. for results in self.testsuite_results:
  729. failures += len(results.failures)
  730. errors += len(results.errors)
  731. skipped += len(results.skipped)
  732. passed += results.passed
  733. if not results.failures and not results.errors and not results.skipped:
  734. continue
  735. no_problems_found = False
  736. print_header(
  737. u"*** {0} ".format(results.header),
  738. sep=u"*",
  739. inline=True,
  740. width=self.options.output_columns,
  741. )
  742. if results.skipped:
  743. print_header(
  744. u" -------- Skipped Tests ",
  745. sep="-",
  746. inline=True,
  747. width=self.options.output_columns,
  748. )
  749. maxlen = len(
  750. max([testcase.id for testcase in results.skipped], key=len)
  751. )
  752. fmt = u" -> {0: <{maxlen}} -> {1}"
  753. for testcase in results.skipped:
  754. print(fmt.format(testcase.id, testcase.reason, maxlen=maxlen))
  755. print_header(
  756. u" ", sep="-", inline=True, width=self.options.output_columns
  757. )
  758. if results.errors:
  759. print_header(
  760. u" -------- Tests with Errors ",
  761. sep="-",
  762. inline=True,
  763. width=self.options.output_columns,
  764. )
  765. for testcase in results.errors:
  766. print_header(
  767. u" -> {0} ".format(testcase.id),
  768. sep=u".",
  769. inline=True,
  770. width=self.options.output_columns,
  771. )
  772. for line in testcase.reason.rstrip().splitlines():
  773. print(" {0}".format(line.rstrip()))
  774. print_header(
  775. u" ", sep=u".", inline=True, width=self.options.output_columns
  776. )
  777. print_header(
  778. u" ", sep="-", inline=True, width=self.options.output_columns
  779. )
  780. if results.failures:
  781. print_header(
  782. u" -------- Failed Tests ",
  783. sep="-",
  784. inline=True,
  785. width=self.options.output_columns,
  786. )
  787. for testcase in results.failures:
  788. print_header(
  789. u" -> {0} ".format(testcase.id),
  790. sep=u".",
  791. inline=True,
  792. width=self.options.output_columns,
  793. )
  794. for line in testcase.reason.rstrip().splitlines():
  795. print(" {0}".format(line.rstrip()))
  796. print_header(
  797. u" ", sep=u".", inline=True, width=self.options.output_columns
  798. )
  799. print_header(
  800. u" ", sep="-", inline=True, width=self.options.output_columns
  801. )
  802. if no_problems_found:
  803. print_header(
  804. u"*** No Problems Found While Running Tests ",
  805. sep=u"*",
  806. inline=True,
  807. width=self.options.output_columns,
  808. )
  809. print_header(u"", sep=u"=", inline=True, width=self.options.output_columns)
  810. total = sum([passed, skipped, errors, failures])
  811. print(
  812. "{0} (total={1}, skipped={2}, passed={3}, failures={4}, "
  813. "errors={5}) ".format(
  814. (errors or failures) and "FAILED" or "OK",
  815. total,
  816. skipped,
  817. passed,
  818. failures,
  819. errors,
  820. )
  821. )
  822. print_header(
  823. " Overall Tests Report ",
  824. sep="=",
  825. centered=True,
  826. inline=True,
  827. width=self.options.output_columns,
  828. )
  829. def post_execution_cleanup(self):
  830. """
  831. Run any final clean-up operations. If sub-classed, don't forget to
  832. call SaltTestingParser.post_execution_cleanup(self) from the overridden
  833. method.
  834. """
  835. def finalize(self, exit_code=0):
  836. """
  837. Run the finalization procedures. Show report, clean-up file-system, etc
  838. """
  839. # Collect any child processes still laying around
  840. children = processes.collect_child_processes(os.getpid())
  841. if self.options.no_report is False:
  842. self.print_overall_testsuite_report()
  843. self.post_execution_cleanup()
  844. # Brute force approach to terminate this process and its children
  845. if children:
  846. log.info("Terminating test suite child processes: %s", children)
  847. processes.terminate_process(children=children, kill_children=True)
  848. children = processes.collect_child_processes(os.getpid())
  849. if children:
  850. log.info(
  851. "Second run at terminating test suite child processes: %s", children
  852. )
  853. processes.terminate_process(children=children, kill_children=True)
  854. exit_msg = "Test suite execution finalized with exit code: {}".format(exit_code)
  855. log.info(exit_msg)
  856. self.exit(status=exit_code, msg=exit_msg + "\n")
  857. def run_suite_in_docker(self):
  858. """
  859. Run the tests suite in a Docker container
  860. """
  861. def stop_running_docked_container(cid, signum=None, frame=None):
  862. # Allow some time for the container to stop if it's going to be
  863. # stopped by docker or any signals docker might have received
  864. time.sleep(0.5)
  865. print_header("", inline=True, width=self.options.output_columns)
  866. # Let's check if, in fact, the container is stopped
  867. scode_call = subprocess.Popen(
  868. [
  869. self.options.docker_binary,
  870. "inspect",
  871. "--format={{.State.Running}}",
  872. cid,
  873. ],
  874. env=os.environ.copy(),
  875. close_fds=True,
  876. stdout=subprocess.PIPE,
  877. )
  878. scode_call.wait()
  879. parsed_scode = scode_call.stdout.read().strip()
  880. if six.PY3:
  881. parsed_scode = parsed_scode.decode(__salt_system_encoding__)
  882. if parsed_scode != "false":
  883. # If the container is still running, let's make sure it
  884. # properly stops
  885. sys.stdout.write(" * Making sure the container is stopped. CID: ")
  886. sys.stdout.flush()
  887. stop_call = subprocess.Popen(
  888. [self.options.docker_binary, "stop", "--time=15", cid],
  889. env=os.environ.copy(),
  890. close_fds=True,
  891. stdout=subprocess.PIPE,
  892. )
  893. stop_call.wait()
  894. output = stop_call.stdout.read().strip()
  895. if six.PY3:
  896. output = output.decode(__salt_system_encoding__)
  897. print(output)
  898. sys.stdout.flush()
  899. time.sleep(0.5)
  900. # Let's get the container's exit code. We can't trust on Popen's
  901. # returncode because it's not reporting the proper one? Still
  902. # haven't narrowed it down why.
  903. sys.stdout.write(" * Container exit code: ")
  904. sys.stdout.flush()
  905. rcode_call = subprocess.Popen(
  906. [
  907. self.options.docker_binary,
  908. "inspect",
  909. "--format={{.State.ExitCode}}",
  910. cid,
  911. ],
  912. env=os.environ.copy(),
  913. close_fds=True,
  914. stdout=subprocess.PIPE,
  915. )
  916. rcode_call.wait()
  917. parsed_rcode = rcode_call.stdout.read().strip()
  918. if six.PY3:
  919. parsed_rcode = parsed_rcode.decode(__salt_system_encoding__)
  920. try:
  921. returncode = int(parsed_rcode)
  922. except ValueError:
  923. returncode = -1
  924. print(parsed_rcode)
  925. sys.stdout.flush()
  926. if self.options.docked_skip_delete is False and (
  927. self.options.docked_skip_delete_on_errors is False
  928. or (self.options.docked_skip_delete_on_error and returncode == 0)
  929. ):
  930. sys.stdout.write(" * Cleaning Up Temporary Docker Container. CID: ")
  931. sys.stdout.flush()
  932. cleanup_call = subprocess.Popen(
  933. [self.options.docker_binary, "rm", cid],
  934. env=os.environ.copy(),
  935. close_fds=True,
  936. stdout=subprocess.PIPE,
  937. )
  938. cleanup_call.wait()
  939. output = cleanup_call.stdout.read().strip()
  940. if six.PY3:
  941. output = output.decode(__salt_system_encoding__)
  942. print(output)
  943. if "DOCKER_CIDFILE" not in os.environ:
  944. # The CID file was not created "from the outside", so delete it
  945. os.unlink(cidfile)
  946. print_header("", inline=True, width=self.options.output_columns)
  947. # Finally, EXIT!
  948. sys.exit(returncode)
  949. # Let's start the Docker container and run the tests suite there
  950. if "/" not in self.options.docked:
  951. container = "salttest/{0}".format(self.options.docked)
  952. else:
  953. container = self.options.docked
  954. calling_args = [
  955. self.options.docked_interpreter,
  956. "/salt-source/tests/runtests.py",
  957. ]
  958. for option in self._get_all_options():
  959. if option.dest is None:
  960. # For example --version
  961. continue
  962. if option.dest and (
  963. option.dest in ("verbosity",) or option.dest.startswith("docked")
  964. ):
  965. # We don't need to pass any docker related arguments inside the
  966. # container, and verbose will be handled bellow
  967. continue
  968. default = self.defaults.get(option.dest)
  969. value = getattr(self.options, option.dest, default)
  970. if default == value:
  971. # This is the default value, no need to pass the option to the
  972. # parser
  973. continue
  974. if option.action.startswith("store_"):
  975. calling_args.append(option.get_opt_string())
  976. elif option.action == "append":
  977. for val in value is not None and value or default:
  978. calling_args.extend([option.get_opt_string(), str(val)])
  979. elif option.action == "count":
  980. calling_args.extend([option.get_opt_string()] * value)
  981. else:
  982. calling_args.extend(
  983. [
  984. option.get_opt_string(),
  985. str(value is not None and value or default),
  986. ]
  987. )
  988. if not self.options.run_destructive:
  989. calling_args.append("--run-destructive")
  990. if self.options.verbosity > 1:
  991. calling_args.append("-{0}".format("v" * (self.options.verbosity - 1)))
  992. sys.stdout.write(" * Docker command: {0}\n".format(" ".join(calling_args)))
  993. sys.stdout.write(
  994. " * Running the tests suite under the {0!r} docker "
  995. "container. CID: ".format(container)
  996. )
  997. sys.stdout.flush()
  998. cidfile = os.environ.get(
  999. "DOCKER_CIDFILE", tempfile.mktemp(prefix="docked-testsuite-", suffix=".cid")
  1000. )
  1001. call = subprocess.Popen(
  1002. [
  1003. self.options.docker_binary,
  1004. "run",
  1005. # '--rm=true', Do not remove the container automatically, we need
  1006. # to get information back, even for stopped containers
  1007. "--tty",
  1008. "--interactive",
  1009. "-v",
  1010. "{0}:/salt-source".format(self.source_code_basedir),
  1011. "-w",
  1012. "/salt-source",
  1013. "-e",
  1014. "SHELL=/bin/sh",
  1015. "-e",
  1016. "COLUMNS={0}".format(WIDTH),
  1017. "-e",
  1018. "LINES={0}".format(HEIGHT),
  1019. "--cidfile={0}".format(cidfile),
  1020. container,
  1021. # We need to pass the runtests.py arguments as a single string so
  1022. # that the start-me-up.sh script can handle them properly
  1023. " ".join(calling_args),
  1024. ],
  1025. env=os.environ.copy(),
  1026. close_fds=True,
  1027. )
  1028. cid = None
  1029. cid_printed = terminating = exiting = False
  1030. signal_handler_installed = signalled = False
  1031. time.sleep(0.25)
  1032. while True:
  1033. try:
  1034. time.sleep(0.15)
  1035. if cid_printed is False:
  1036. # pylint: disable=resource-leakage
  1037. with open(cidfile) as cidfile_fd:
  1038. cid = cidfile_fd.read()
  1039. if cid:
  1040. print(cid)
  1041. sys.stdout.flush()
  1042. cid_printed = True
  1043. # Install our signal handler to properly shutdown
  1044. # the docker container
  1045. for sig in (
  1046. signal.SIGTERM,
  1047. signal.SIGINT,
  1048. signal.SIGHUP,
  1049. signal.SIGQUIT,
  1050. ):
  1051. signal.signal(
  1052. sig, partial(stop_running_docked_container, cid)
  1053. )
  1054. signal_handler_installed = True
  1055. # pylint: enable=resource-leakage
  1056. if exiting:
  1057. break
  1058. elif terminating and not exiting:
  1059. exiting = True
  1060. call.kill()
  1061. break
  1062. elif signalled and not terminating:
  1063. terminating = True
  1064. call.terminate()
  1065. else:
  1066. call.poll()
  1067. if call.returncode is not None:
  1068. # Finished
  1069. break
  1070. except KeyboardInterrupt:
  1071. print("Caught CTRL-C, exiting...")
  1072. signalled = True
  1073. call.send_signal(signal.SIGINT)
  1074. call.wait()
  1075. time.sleep(0.25)
  1076. # Finish up
  1077. if signal_handler_installed:
  1078. stop_running_docked_container(
  1079. cid, signum=(signal.SIGINT if signalled else WEIRD_SIGNAL_NUM)
  1080. )
  1081. else:
  1082. sys.exit(call.returncode)
  1083. class SaltTestcaseParser(SaltTestingParser):
  1084. """
  1085. Option parser to run one or more ``unittest.case.TestCase``, ie, no
  1086. discovery involved.
  1087. """
  1088. def __init__(self, *args, **kwargs):
  1089. SaltTestingParser.__init__(self, None, *args, **kwargs)
  1090. self.usage = "%prog [options]"
  1091. self.option_groups.remove(self.test_selection_group)
  1092. if self.has_option("--xml-out"):
  1093. self.remove_option("--xml-out")
  1094. def get_prog_name(self):
  1095. return "{0} {1}".format(sys.executable.split(os.sep)[-1], sys.argv[0])
  1096. def run_testcase(self, testcase):
  1097. """
  1098. Run one or more ``unittest.case.TestCase``
  1099. """
  1100. header = ""
  1101. loader = TestLoader()
  1102. if isinstance(testcase, list):
  1103. for case in testcase:
  1104. tests = loader.loadTestsFromTestCase(case)
  1105. else:
  1106. tests = loader.loadTestsFromTestCase(testcase)
  1107. if not isinstance(testcase, list):
  1108. header = "{0} Tests".format(testcase.__name__)
  1109. print_header(
  1110. "Starting {0}".format(header), width=self.options.output_columns
  1111. )
  1112. runner = TextTestRunner(
  1113. verbosity=self.options.verbosity, failfast=self.options.failfast,
  1114. ).run(tests)
  1115. self.testsuite_results.append((header, runner))
  1116. return runner.wasSuccessful()