__init__.py 43 KB

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