1
0

noxfile.py 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145
  1. # -*- coding: utf-8 -*-
  2. '''
  3. noxfile
  4. ~~~~~~~
  5. Nox configuration script
  6. '''
  7. # pylint: disable=resource-leakage
  8. # Import Python libs
  9. from __future__ import absolute_import, unicode_literals, print_function
  10. import os
  11. import sys
  12. import glob
  13. import json
  14. import pprint
  15. import shutil
  16. import tempfile
  17. import datetime
  18. if __name__ == '__main__':
  19. sys.stderr.write('Do not execute this file directly. Use nox instead, it will know how to handle this file\n')
  20. sys.stderr.flush()
  21. exit(1)
  22. # Import 3rd-party libs
  23. import nox
  24. from nox.command import CommandFailed
  25. IS_PY3 = sys.version_info > (2,)
  26. # Be verbose when runing under a CI context
  27. PIP_INSTALL_SILENT = (os.environ.get('JENKINS_URL') or os.environ.get('CI') or os.environ.get('DRONE')) is None
  28. # Global Path Definitions
  29. REPO_ROOT = os.path.abspath(os.path.dirname(__file__))
  30. SITECUSTOMIZE_DIR = os.path.join(REPO_ROOT, 'tests', 'support', 'coverage')
  31. IS_DARWIN = sys.platform.lower().startswith('darwin')
  32. IS_WINDOWS = sys.platform.lower().startswith('win')
  33. # Python versions to run against
  34. _PYTHON_VERSIONS = ('2', '2.7', '3', '3.4', '3.5', '3.6', '3.7')
  35. # Nox options
  36. # Reuse existing virtualenvs
  37. nox.options.reuse_existing_virtualenvs = True
  38. # Don't fail on missing interpreters
  39. nox.options.error_on_missing_interpreters = False
  40. # Change current directory to REPO_ROOT
  41. os.chdir(REPO_ROOT)
  42. RUNTESTS_LOGFILE = os.path.join(
  43. 'artifacts', 'logs',
  44. 'runtests-{}.log'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S.%f'))
  45. )
  46. # Prevent Python from writing bytecode
  47. os.environ[str('PYTHONDONTWRITEBYTECODE')] = str('1')
  48. def _create_ci_directories():
  49. for dirname in ('logs', 'coverage', 'xml-unittests-output'):
  50. path = os.path.join('artifacts', dirname)
  51. if not os.path.exists(path):
  52. os.makedirs(path)
  53. def _get_session_python_version_info(session):
  54. try:
  55. version_info = session._runner._real_python_version_info
  56. except AttributeError:
  57. old_install_only_value = session._runner.global_config.install_only
  58. try:
  59. # Force install only to be false for the following chunk of code
  60. # For additional information as to why see:
  61. # https://github.com/theacodes/nox/pull/181
  62. session._runner.global_config.install_only = False
  63. session_py_version = session.run(
  64. 'python', '-c'
  65. 'import sys; sys.stdout.write("{}.{}.{}".format(*sys.version_info))',
  66. silent=True,
  67. log=False,
  68. )
  69. version_info = tuple(int(part) for part in session_py_version.split('.') if part.isdigit())
  70. session._runner._real_python_version_info = version_info
  71. finally:
  72. session._runner.global_config.install_only = old_install_only_value
  73. return version_info
  74. def _get_session_python_site_packages_dir(session):
  75. try:
  76. site_packages_dir = session._runner._site_packages_dir
  77. except AttributeError:
  78. old_install_only_value = session._runner.global_config.install_only
  79. try:
  80. # Force install only to be false for the following chunk of code
  81. # For additional information as to why see:
  82. # https://github.com/theacodes/nox/pull/181
  83. session._runner.global_config.install_only = False
  84. site_packages_dir = session.run(
  85. 'python', '-c'
  86. 'import sys; from distutils.sysconfig import get_python_lib; sys.stdout.write(get_python_lib())',
  87. silent=True,
  88. log=False,
  89. )
  90. session._runner._site_packages_dir = site_packages_dir
  91. finally:
  92. session._runner.global_config.install_only = old_install_only_value
  93. return site_packages_dir
  94. def _get_pydir(session):
  95. version_info = _get_session_python_version_info(session)
  96. if version_info < (2, 7):
  97. session.error('Only Python >= 2.7 is supported')
  98. return 'py{}.{}'.format(*version_info)
  99. def _get_distro_info(session):
  100. try:
  101. distro = session._runner._distro
  102. except AttributeError:
  103. # The distro package doesn't output anything for Windows
  104. old_install_only_value = session._runner.global_config.install_only
  105. try:
  106. # Force install only to be false for the following chunk of code
  107. # For additional information as to why see:
  108. # https://github.com/theacodes/nox/pull/181
  109. session._runner.global_config.install_only = False
  110. session.install('--progress-bar=off', 'distro', silent=PIP_INSTALL_SILENT)
  111. output = session.run('distro', '-j', silent=True)
  112. distro = json.loads(output.strip())
  113. session.log('Distro information:\n%s', pprint.pformat(distro))
  114. session._runner._distro = distro
  115. finally:
  116. session._runner.global_config.install_only = old_install_only_value
  117. return distro
  118. def _install_system_packages(session):
  119. '''
  120. Because some python packages are provided by the distribution and cannot
  121. be pip installed, and because we don't want the whole system python packages
  122. on our virtualenvs, we copy the required system python packages into
  123. the virtualenv
  124. '''
  125. system_python_packages = {
  126. '__debian_based_distros__': [
  127. '/usr/lib/python{py_version}/dist-packages/*apt*'
  128. ]
  129. }
  130. distro = _get_distro_info(session)
  131. if not distro['id'].startswith(('debian', 'ubuntu')):
  132. # This only applies to debian based distributions
  133. return
  134. system_python_packages['{id}-{version}'.format(**distro)] = \
  135. system_python_packages['{id}-{version_parts[major]}'.format(**distro)] = \
  136. system_python_packages['__debian_based_distros__'][:]
  137. distro_keys = [
  138. '{id}'.format(**distro),
  139. '{id}-{version}'.format(**distro),
  140. '{id}-{version_parts[major]}'.format(**distro)
  141. ]
  142. version_info = _get_session_python_version_info(session)
  143. py_version_keys = [
  144. '{}'.format(*version_info),
  145. '{}.{}'.format(*version_info)
  146. ]
  147. session_site_packages_dir = _get_session_python_site_packages_dir(session)
  148. for distro_key in distro_keys:
  149. if distro_key not in system_python_packages:
  150. continue
  151. patterns = system_python_packages[distro_key]
  152. for pattern in patterns:
  153. for py_version in py_version_keys:
  154. matches = set(glob.glob(pattern.format(py_version=py_version)))
  155. if not matches:
  156. continue
  157. for match in matches:
  158. src = os.path.realpath(match)
  159. dst = os.path.join(session_site_packages_dir, os.path.basename(match))
  160. if os.path.exists(dst):
  161. session.log('Not overwritting already existing %s with %s', dst, src)
  162. continue
  163. session.log('Copying %s into %s', src, dst)
  164. if os.path.isdir(src):
  165. shutil.copytree(src, dst)
  166. else:
  167. shutil.copyfile(src, dst)
  168. def _get_distro_pip_constraints(session, transport):
  169. # Install requirements
  170. distro_constraints = []
  171. if transport == 'tcp':
  172. # The TCP requirements are the exact same requirements as the ZeroMQ ones
  173. transport = 'zeromq'
  174. pydir = _get_pydir(session)
  175. if IS_WINDOWS:
  176. _distro_constraints = os.path.join('requirements',
  177. 'static',
  178. pydir,
  179. '{}-windows.txt'.format(transport))
  180. if os.path.exists(_distro_constraints):
  181. distro_constraints.append(_distro_constraints)
  182. _distro_constraints = os.path.join('requirements',
  183. 'static',
  184. pydir,
  185. 'windows.txt')
  186. if os.path.exists(_distro_constraints):
  187. distro_constraints.append(_distro_constraints)
  188. _distro_constraints = os.path.join('requirements',
  189. 'static',
  190. pydir,
  191. 'windows-crypto.txt')
  192. if os.path.exists(_distro_constraints):
  193. distro_constraints.append(_distro_constraints)
  194. elif IS_DARWIN:
  195. _distro_constraints = os.path.join('requirements',
  196. 'static',
  197. pydir,
  198. '{}-darwin.txt'.format(transport))
  199. if os.path.exists(_distro_constraints):
  200. distro_constraints.append(_distro_constraints)
  201. _distro_constraints = os.path.join('requirements',
  202. 'static',
  203. pydir,
  204. 'darwin.txt')
  205. if os.path.exists(_distro_constraints):
  206. distro_constraints.append(_distro_constraints)
  207. _distro_constraints = os.path.join('requirements',
  208. 'static',
  209. pydir,
  210. 'darwin-crypto.txt')
  211. if os.path.exists(_distro_constraints):
  212. distro_constraints.append(_distro_constraints)
  213. else:
  214. _install_system_packages(session)
  215. distro = _get_distro_info(session)
  216. distro_keys = [
  217. 'linux',
  218. '{id}'.format(**distro),
  219. '{id}-{version}'.format(**distro),
  220. '{id}-{version_parts[major]}'.format(**distro)
  221. ]
  222. for distro_key in distro_keys:
  223. _distro_constraints = os.path.join('requirements',
  224. 'static',
  225. pydir,
  226. '{}.txt'.format(distro_key))
  227. if os.path.exists(_distro_constraints):
  228. distro_constraints.append(_distro_constraints)
  229. _distro_constraints = os.path.join('requirements',
  230. 'static',
  231. pydir,
  232. '{}-crypto.txt'.format(distro_key))
  233. if os.path.exists(_distro_constraints):
  234. distro_constraints.append(_distro_constraints)
  235. _distro_constraints = os.path.join('requirements',
  236. 'static',
  237. pydir,
  238. '{}-{}.txt'.format(transport, distro_key))
  239. if os.path.exists(_distro_constraints):
  240. distro_constraints.append(_distro_constraints)
  241. distro_constraints.append(_distro_constraints)
  242. _distro_constraints = os.path.join('requirements',
  243. 'static',
  244. pydir,
  245. '{}-{}-crypto.txt'.format(transport, distro_key))
  246. if os.path.exists(_distro_constraints):
  247. distro_constraints.append(_distro_constraints)
  248. return distro_constraints
  249. def _install_requirements(session, transport, *extra_requirements):
  250. # Install requirements
  251. distro_constraints = _get_distro_pip_constraints(session, transport)
  252. _requirements_files = [
  253. os.path.join('requirements', 'base.txt'),
  254. os.path.join('requirements', 'zeromq.txt'),
  255. os.path.join('requirements', 'pytest.txt')
  256. ]
  257. if sys.platform.startswith('linux'):
  258. requirements_files = [
  259. os.path.join('requirements', 'static', 'linux.in')
  260. ]
  261. elif sys.platform.startswith('win'):
  262. requirements_files = [
  263. os.path.join('pkg', 'windows', 'req.txt'),
  264. os.path.join('requirements', 'static', 'windows.in')
  265. ]
  266. elif sys.platform.startswith('darwin'):
  267. requirements_files = [
  268. os.path.join('pkg', 'osx', 'req.txt'),
  269. os.path.join('pkg', 'osx', 'req_ext.txt'),
  270. os.path.join('pkg', 'osx', 'req_pyobjc.txt'),
  271. os.path.join('requirements', 'static', 'darwin.in')
  272. ]
  273. while True:
  274. if not requirements_files:
  275. break
  276. requirements_file = requirements_files.pop(0)
  277. if requirements_file not in _requirements_files:
  278. _requirements_files.append(requirements_file)
  279. session.log('Processing {}'.format(requirements_file))
  280. with open(requirements_file) as rfh: # pylint: disable=resource-leakage
  281. for line in rfh:
  282. line = line.strip()
  283. if not line:
  284. continue
  285. if line.startswith('-r'):
  286. reqfile = os.path.join(os.path.dirname(requirements_file), line.strip().split()[-1])
  287. if reqfile in _requirements_files:
  288. continue
  289. _requirements_files.append(reqfile)
  290. continue
  291. for requirements_file in _requirements_files:
  292. install_command = [
  293. '--progress-bar=off', '-r', requirements_file
  294. ]
  295. for distro_constraint in distro_constraints:
  296. install_command.extend([
  297. '--constraint', distro_constraint
  298. ])
  299. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  300. if extra_requirements:
  301. install_command = [
  302. '--progress-bar=off',
  303. ]
  304. for distro_constraint in distro_constraints:
  305. install_command.extend([
  306. '--constraint', distro_constraint
  307. ])
  308. install_command += list(extra_requirements)
  309. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  310. def _run_with_coverage(session, *test_cmd):
  311. session.install('--progress-bar=off', 'coverage==5.0.1', silent=PIP_INSTALL_SILENT)
  312. session.run('coverage', 'erase')
  313. python_path_env_var = os.environ.get('PYTHONPATH') or None
  314. if python_path_env_var is None:
  315. python_path_env_var = SITECUSTOMIZE_DIR
  316. else:
  317. python_path_entries = python_path_env_var.split(os.pathsep)
  318. if SITECUSTOMIZE_DIR in python_path_entries:
  319. python_path_entries.remove(SITECUSTOMIZE_DIR)
  320. python_path_entries.insert(0, SITECUSTOMIZE_DIR)
  321. python_path_env_var = os.pathsep.join(python_path_entries)
  322. env = {
  323. # The updated python path so that sitecustomize is importable
  324. 'PYTHONPATH': python_path_env_var,
  325. # The full path to the .coverage data file. Makes sure we always write
  326. # them to the same directory
  327. 'COVERAGE_FILE': os.path.abspath(os.path.join(REPO_ROOT, '.coverage')),
  328. # Instruct sub processes to also run under coverage
  329. 'COVERAGE_PROCESS_START': os.path.join(REPO_ROOT, '.coveragerc')
  330. }
  331. if IS_DARWIN:
  332. # Don't nuke our multiprocessing efforts objc!
  333. # https://stackoverflow.com/questions/50168647/multiprocessing-causes-python-to-crash-and-gives-an-error-may-have-been-in-progr
  334. env['OBJC_DISABLE_INITIALIZE_FORK_SAFETY'] = 'YES'
  335. try:
  336. session.run(*test_cmd, env=env)
  337. finally:
  338. # Always combine and generate the XML coverage report
  339. try:
  340. session.run('coverage', 'combine')
  341. except CommandFailed:
  342. # Sometimes some of the coverage files are corrupt which would trigger a CommandFailed
  343. # exception
  344. pass
  345. # Generate report for salt code coverage
  346. session.run(
  347. 'coverage', 'xml',
  348. '-o', os.path.join('artifacts', 'coverage', 'salt.xml'),
  349. '--omit=tests/*',
  350. '--include=salt/*'
  351. )
  352. # Generate report for tests code coverage
  353. session.run(
  354. 'coverage', 'xml',
  355. '-o', os.path.join('artifacts', 'coverage', 'tests.xml'),
  356. '--omit=salt/*',
  357. '--include=tests/*'
  358. )
  359. def _runtests(session, coverage, cmd_args):
  360. # Create required artifacts directories
  361. _create_ci_directories()
  362. try:
  363. if coverage is True:
  364. _run_with_coverage(session, 'coverage', 'run', os.path.join('tests', 'runtests.py'), *cmd_args)
  365. else:
  366. cmd_args = ['python', os.path.join('tests', 'runtests.py')] + list(cmd_args)
  367. env = None
  368. if IS_DARWIN:
  369. # Don't nuke our multiprocessing efforts objc!
  370. # https://stackoverflow.com/questions/50168647/multiprocessing-causes-python-to-crash-and-gives-an-error-may-have-been-in-progr
  371. env = {'OBJC_DISABLE_INITIALIZE_FORK_SAFETY': 'YES'}
  372. session.run(*cmd_args, env=env)
  373. except CommandFailed: # pylint: disable=try-except-raise
  374. # Disabling re-running failed tests for the time being
  375. raise
  376. # pylint: disable=unreachable
  377. names_file_path = os.path.join('artifacts', 'failed-tests.txt')
  378. session.log('Re-running failed tests if possible')
  379. session.install('--progress-bar=off', 'xunitparser==1.3.3', silent=PIP_INSTALL_SILENT)
  380. session.run(
  381. 'python',
  382. os.path.join('tests', 'support', 'generate-names-file-from-failed-test-reports.py'),
  383. names_file_path
  384. )
  385. if not os.path.exists(names_file_path):
  386. session.log(
  387. 'Failed tests file(%s) was not found. Not rerunning failed tests.',
  388. names_file_path
  389. )
  390. # raise the original exception
  391. raise
  392. with open(names_file_path) as rfh:
  393. contents = rfh.read().strip()
  394. if not contents:
  395. session.log(
  396. 'The failed tests file(%s) is empty. Not rerunning failed tests.',
  397. names_file_path
  398. )
  399. # raise the original exception
  400. raise
  401. failed_tests_count = len(contents.splitlines())
  402. if failed_tests_count > 500:
  403. # 500 test failures?! Something else must have gone wrong, don't even bother
  404. session.error(
  405. 'Total failed tests({}) > 500. No point on re-running the failed tests'.format(
  406. failed_tests_count
  407. )
  408. )
  409. for idx, flag in enumerate(cmd_args[:]):
  410. if '--names-file=' in flag:
  411. cmd_args.pop(idx)
  412. break
  413. elif flag == '--names-file':
  414. cmd_args.pop(idx) # pop --names-file
  415. cmd_args.pop(idx) # pop the actual names file
  416. break
  417. cmd_args.append('--names-file={}'.format(names_file_path))
  418. if coverage is True:
  419. _run_with_coverage(session, 'coverage', 'run', '-m', 'tests.runtests', *cmd_args)
  420. else:
  421. session.run('python', os.path.join('tests', 'runtests.py'), *cmd_args)
  422. # pylint: enable=unreachable
  423. @nox.session(python=_PYTHON_VERSIONS, name='runtests-parametrized')
  424. @nox.parametrize('coverage', [False, True])
  425. @nox.parametrize('transport', ['zeromq', 'tcp'])
  426. @nox.parametrize('crypto', [None, 'm2crypto', 'pycryptodomex'])
  427. def runtests_parametrized(session, coverage, transport, crypto):
  428. # Install requirements
  429. _install_requirements(session, transport, 'unittest-xml-reporting==2.5.2')
  430. if crypto:
  431. if crypto == 'm2crypto':
  432. session.run('pip', 'uninstall', '-y', 'pycrypto', 'pycryptodome', 'pycryptodomex', silent=True)
  433. else:
  434. session.run('pip', 'uninstall', '-y', 'm2crypto', silent=True)
  435. distro_constraints = _get_distro_pip_constraints(session, transport)
  436. install_command = [
  437. '--progress-bar=off',
  438. ]
  439. for distro_constraint in distro_constraints:
  440. install_command.extend([
  441. '--constraint', distro_constraint
  442. ])
  443. install_command.append(crypto)
  444. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  445. cmd_args = [
  446. '--tests-logfile={}'.format(RUNTESTS_LOGFILE),
  447. '--transport={}'.format(transport)
  448. ] + session.posargs
  449. _runtests(session, coverage, cmd_args)
  450. @nox.session(python=_PYTHON_VERSIONS)
  451. @nox.parametrize('coverage', [False, True])
  452. def runtests(session, coverage):
  453. '''
  454. runtests.py session with zeromq transport and default crypto
  455. '''
  456. session.notify(
  457. 'runtests-parametrized-{}(coverage={}, crypto=None, transport=\'zeromq\')'.format(
  458. session.python,
  459. coverage
  460. )
  461. )
  462. @nox.session(python=_PYTHON_VERSIONS, name='runtests-tcp')
  463. @nox.parametrize('coverage', [False, True])
  464. def runtests_tcp(session, coverage):
  465. '''
  466. runtests.py session with TCP transport and default crypto
  467. '''
  468. session.notify(
  469. 'runtests-parametrized-{}(coverage={}, crypto=None, transport=\'tcp\')'.format(
  470. session.python,
  471. coverage
  472. )
  473. )
  474. @nox.session(python=_PYTHON_VERSIONS, name='runtests-zeromq')
  475. @nox.parametrize('coverage', [False, True])
  476. def runtests_zeromq(session, coverage):
  477. '''
  478. runtests.py session with zeromq transport and default crypto
  479. '''
  480. session.notify(
  481. 'runtests-parametrized-{}(coverage={}, crypto=None, transport=\'zeromq\')'.format(
  482. session.python,
  483. coverage
  484. )
  485. )
  486. @nox.session(python=_PYTHON_VERSIONS, name='runtests-m2crypto')
  487. @nox.parametrize('coverage', [False, True])
  488. def runtests_m2crypto(session, coverage):
  489. '''
  490. runtests.py session with zeromq transport and m2crypto
  491. '''
  492. session.notify(
  493. 'runtests-parametrized-{}(coverage={}, crypto=\'m2crypto\', transport=\'zeromq\')'.format(
  494. session.python,
  495. coverage
  496. )
  497. )
  498. @nox.session(python=_PYTHON_VERSIONS, name='runtests-tcp-m2crypto')
  499. @nox.parametrize('coverage', [False, True])
  500. def runtests_tcp_m2crypto(session, coverage):
  501. '''
  502. runtests.py session with TCP transport and m2crypto
  503. '''
  504. session.notify(
  505. 'runtests-parametrized-{}(coverage={}, crypto=\'m2crypto\', transport=\'tcp\')'.format(
  506. session.python,
  507. coverage
  508. )
  509. )
  510. @nox.session(python=_PYTHON_VERSIONS, name='runtests-zeromq-m2crypto')
  511. @nox.parametrize('coverage', [False, True])
  512. def runtests_zeromq_m2crypto(session, coverage):
  513. '''
  514. runtests.py session with zeromq transport and m2crypto
  515. '''
  516. session.notify(
  517. 'runtests-parametrized-{}(coverage={}, crypto=\'m2crypto\', transport=\'zeromq\')'.format(
  518. session.python,
  519. coverage
  520. )
  521. )
  522. @nox.session(python=_PYTHON_VERSIONS, name='runtests-pycryptodomex')
  523. @nox.parametrize('coverage', [False, True])
  524. def runtests_pycryptodomex(session, coverage):
  525. '''
  526. runtests.py session with zeromq transport and pycryptodomex
  527. '''
  528. session.notify(
  529. 'runtests-parametrized-{}(coverage={}, crypto=\'pycryptodomex\', transport=\'zeromq\')'.format(
  530. session.python,
  531. coverage
  532. )
  533. )
  534. @nox.session(python=_PYTHON_VERSIONS, name='runtests-tcp-pycryptodomex')
  535. @nox.parametrize('coverage', [False, True])
  536. def runtests_tcp_pycryptodomex(session, coverage):
  537. '''
  538. runtests.py session with TCP transport and pycryptodomex
  539. '''
  540. session.notify(
  541. 'runtests-parametrized-{}(coverage={}, crypto=\'pycryptodomex\', transport=\'tcp\')'.format(
  542. session.python,
  543. coverage
  544. )
  545. )
  546. @nox.session(python=_PYTHON_VERSIONS, name='runtests-zeromq-pycryptodomex')
  547. @nox.parametrize('coverage', [False, True])
  548. def runtests_zeromq_pycryptodomex(session, coverage):
  549. '''
  550. runtests.py session with zeromq transport and pycryptodomex
  551. '''
  552. session.notify(
  553. 'runtests-parametrized-{}(coverage={}, crypto=\'pycryptodomex\', transport=\'zeromq\')'.format(
  554. session.python,
  555. coverage
  556. )
  557. )
  558. @nox.session(python=_PYTHON_VERSIONS, name='runtests-cloud')
  559. @nox.parametrize('coverage', [False, True])
  560. def runtests_cloud(session, coverage):
  561. # Install requirements
  562. _install_requirements(session, 'zeromq', 'unittest-xml-reporting==2.2.1')
  563. pydir = _get_pydir(session)
  564. cloud_requirements = os.path.join('requirements', 'static', pydir, 'cloud.txt')
  565. session.install('--progress-bar=off', '-r', cloud_requirements, silent=PIP_INSTALL_SILENT)
  566. cmd_args = [
  567. '--tests-logfile={}'.format(RUNTESTS_LOGFILE),
  568. '--cloud-provider-tests'
  569. ] + session.posargs
  570. _runtests(session, coverage, cmd_args)
  571. @nox.session(python=_PYTHON_VERSIONS, name='runtests-tornado')
  572. @nox.parametrize('coverage', [False, True])
  573. def runtests_tornado(session, coverage):
  574. # Install requirements
  575. _install_requirements(session, 'zeromq', 'unittest-xml-reporting==2.2.1')
  576. session.install('--progress-bar=off', 'tornado==5.0.2', silent=PIP_INSTALL_SILENT)
  577. session.install('--progress-bar=off', 'pyzmq==17.0.0', silent=PIP_INSTALL_SILENT)
  578. cmd_args = [
  579. '--tests-logfile={}'.format(RUNTESTS_LOGFILE)
  580. ] + session.posargs
  581. _runtests(session, coverage, cmd_args)
  582. @nox.session(python=_PYTHON_VERSIONS, name='pytest-parametrized')
  583. @nox.parametrize('coverage', [False, True])
  584. @nox.parametrize('transport', ['zeromq', 'tcp'])
  585. @nox.parametrize('crypto', [None, 'm2crypto', 'pycryptodomex'])
  586. def pytest_parametrized(session, coverage, transport, crypto):
  587. # Install requirements
  588. _install_requirements(session, transport)
  589. if crypto:
  590. if crypto == 'm2crypto':
  591. session.run('pip', 'uninstall', '-y', 'pycrypto', 'pycryptodome', 'pycryptodomex', silent=True)
  592. else:
  593. session.run('pip', 'uninstall', '-y', 'm2crypto', silent=True)
  594. distro_constraints = _get_distro_pip_constraints(session, transport)
  595. install_command = [
  596. '--progress-bar=off',
  597. ]
  598. for distro_constraint in distro_constraints:
  599. install_command.extend([
  600. '--constraint', distro_constraint
  601. ])
  602. install_command.append(crypto)
  603. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  604. cmd_args = [
  605. '--rootdir', REPO_ROOT,
  606. '--log-file={}'.format(RUNTESTS_LOGFILE),
  607. '--log-file-level=debug',
  608. '--no-print-logs',
  609. '-ra',
  610. '-s',
  611. '--transport={}'.format(transport)
  612. ] + session.posargs
  613. _pytest(session, coverage, cmd_args)
  614. @nox.session(python=_PYTHON_VERSIONS)
  615. @nox.parametrize('coverage', [False, True])
  616. def pytest(session, coverage):
  617. '''
  618. pytest session with zeromq transport and default crypto
  619. '''
  620. session.notify(
  621. 'pytest-parametrized-{}(coverage={}, crypto=None, transport=\'zeromq\')'.format(
  622. session.python,
  623. coverage
  624. )
  625. )
  626. @nox.session(python=_PYTHON_VERSIONS, name='pytest-tcp')
  627. @nox.parametrize('coverage', [False, True])
  628. def pytest_tcp(session, coverage):
  629. '''
  630. pytest session with TCP transport and default crypto
  631. '''
  632. session.notify(
  633. 'pytest-parametrized-{}(coverage={}, crypto=None, transport=\'tcp\')'.format(
  634. session.python,
  635. coverage
  636. )
  637. )
  638. @nox.session(python=_PYTHON_VERSIONS, name='pytest-zeromq')
  639. @nox.parametrize('coverage', [False, True])
  640. def pytest_zeromq(session, coverage):
  641. '''
  642. pytest session with zeromq transport and default crypto
  643. '''
  644. session.notify(
  645. 'pytest-parametrized-{}(coverage={}, crypto=None, transport=\'zeromq\')'.format(
  646. session.python,
  647. coverage
  648. )
  649. )
  650. @nox.session(python=_PYTHON_VERSIONS, name='pytest-m2crypto')
  651. @nox.parametrize('coverage', [False, True])
  652. def pytest_m2crypto(session, coverage):
  653. '''
  654. pytest session with zeromq transport and m2crypto
  655. '''
  656. session.notify(
  657. 'pytest-parametrized-{}(coverage={}, crypto=\'m2crypto\', transport=\'zeromq\')'.format(
  658. session.python,
  659. coverage
  660. )
  661. )
  662. @nox.session(python=_PYTHON_VERSIONS, name='pytest-tcp-m2crypto')
  663. @nox.parametrize('coverage', [False, True])
  664. def pytest_tcp_m2crypto(session, coverage):
  665. '''
  666. pytest session with TCP transport and m2crypto
  667. '''
  668. session.notify(
  669. 'pytest-parametrized-{}(coverage={}, crypto=\'m2crypto\', transport=\'tcp\')'.format(
  670. session.python,
  671. coverage
  672. )
  673. )
  674. @nox.session(python=_PYTHON_VERSIONS, name='pytest-zeromq-m2crypto')
  675. @nox.parametrize('coverage', [False, True])
  676. def pytest_zeromq_m2crypto(session, coverage):
  677. '''
  678. pytest session with zeromq transport and m2crypto
  679. '''
  680. session.notify(
  681. 'pytest-parametrized-{}(coverage={}, crypto=\'m2crypto\', transport=\'zeromq\')'.format(
  682. session.python,
  683. coverage
  684. )
  685. )
  686. @nox.session(python=_PYTHON_VERSIONS, name='pytest-pycryptodomex')
  687. @nox.parametrize('coverage', [False, True])
  688. def pytest_pycryptodomex(session, coverage):
  689. '''
  690. pytest session with zeromq transport and pycryptodomex
  691. '''
  692. session.notify(
  693. 'pytest-parametrized-{}(coverage={}, crypto=\'pycryptodomex\', transport=\'zeromq\')'.format(
  694. session.python,
  695. coverage
  696. )
  697. )
  698. @nox.session(python=_PYTHON_VERSIONS, name='pytest-tcp-pycryptodomex')
  699. @nox.parametrize('coverage', [False, True])
  700. def pytest_tcp_pycryptodomex(session, coverage):
  701. '''
  702. pytest session with TCP transport and pycryptodomex
  703. '''
  704. session.notify(
  705. 'pytest-parametrized-{}(coverage={}, crypto=\'pycryptodomex\', transport=\'tcp\')'.format(
  706. session.python,
  707. coverage
  708. )
  709. )
  710. @nox.session(python=_PYTHON_VERSIONS, name='pytest-zeromq-pycryptodomex')
  711. @nox.parametrize('coverage', [False, True])
  712. def pytest_zeromq_pycryptodomex(session, coverage):
  713. '''
  714. pytest session with zeromq transport and pycryptodomex
  715. '''
  716. session.notify(
  717. 'pytest-parametrized-{}(coverage={}, crypto=\'pycryptodomex\', transport=\'zeromq\')'.format(
  718. session.python,
  719. coverage
  720. )
  721. )
  722. @nox.session(python=_PYTHON_VERSIONS, name='pytest-cloud')
  723. @nox.parametrize('coverage', [False, True])
  724. def pytest_cloud(session, coverage):
  725. # Install requirements
  726. _install_requirements(session, 'zeromq')
  727. pydir = _get_pydir(session)
  728. cloud_requirements = os.path.join('requirements', 'static', pydir, 'cloud.txt')
  729. session.install('--progress-bar=off', '-r', cloud_requirements, silent=PIP_INSTALL_SILENT)
  730. cmd_args = [
  731. '--rootdir', REPO_ROOT,
  732. '--log-file={}'.format(RUNTESTS_LOGFILE),
  733. '--log-file-level=debug',
  734. '--no-print-logs',
  735. '-ra',
  736. '-s',
  737. os.path.join('tests', 'integration', 'cloud', 'providers')
  738. ] + session.posargs
  739. _pytest(session, coverage, cmd_args)
  740. @nox.session(python=_PYTHON_VERSIONS, name='pytest-tornado')
  741. @nox.parametrize('coverage', [False, True])
  742. def pytest_tornado(session, coverage):
  743. # Install requirements
  744. _install_requirements(session, 'zeromq')
  745. session.install('--progress-bar=off', 'tornado==5.0.2', silent=PIP_INSTALL_SILENT)
  746. session.install('--progress-bar=off', 'pyzmq==17.0.0', silent=PIP_INSTALL_SILENT)
  747. cmd_args = [
  748. '--rootdir', REPO_ROOT,
  749. '--log-file={}'.format(RUNTESTS_LOGFILE),
  750. '--log-file-level=debug',
  751. '--no-print-logs',
  752. '-ra',
  753. '-s',
  754. ] + session.posargs
  755. _pytest(session, coverage, cmd_args)
  756. def _pytest(session, coverage, cmd_args):
  757. # Create required artifacts directories
  758. _create_ci_directories()
  759. env = None
  760. if IS_DARWIN:
  761. # Don't nuke our multiprocessing efforts objc!
  762. # https://stackoverflow.com/questions/50168647/multiprocessing-causes-python-to-crash-and-gives-an-error-may-have-been-in-progr
  763. env = {'OBJC_DISABLE_INITIALIZE_FORK_SAFETY': 'YES'}
  764. try:
  765. if coverage is True:
  766. _run_with_coverage(session, 'coverage', 'run', '-m', 'pytest', *cmd_args)
  767. else:
  768. session.run('pytest', *cmd_args, env=env)
  769. except CommandFailed: # pylint: disable=try-except-raise
  770. # Not rerunning failed tests for now
  771. raise
  772. # pylint: disable=unreachable
  773. # Re-run failed tests
  774. session.log('Re-running failed tests')
  775. for idx, parg in enumerate(cmd_args):
  776. if parg.startswith('--junitxml='):
  777. cmd_args[idx] = parg.replace('.xml', '-rerun-failed.xml')
  778. cmd_args.append('--lf')
  779. if coverage is True:
  780. _run_with_coverage(session, 'coverage', 'run', '-m', 'pytest', *cmd_args)
  781. else:
  782. session.run('pytest', *cmd_args, env=env)
  783. # pylint: enable=unreachable
  784. class Tee:
  785. '''
  786. Python class to mimic linux tee behaviour
  787. '''
  788. def __init__(self, first, second):
  789. self._first = first
  790. self._second = second
  791. def write(self, b):
  792. wrote = self._first.write(b)
  793. self._first.flush()
  794. self._second.write(b)
  795. self._second.flush()
  796. def fileno(self):
  797. return self._first.fileno()
  798. def _lint(session, rcfile, flags, paths, tee_output=True):
  799. _install_requirements(session, 'zeromq')
  800. requirements_file = 'requirements/static/lint.in'
  801. distro_constraints = [
  802. 'requirements/static/{}/lint.txt'.format(_get_pydir(session))
  803. ]
  804. install_command = [
  805. '--progress-bar=off', '-r', requirements_file
  806. ]
  807. for distro_constraint in distro_constraints:
  808. install_command.extend([
  809. '--constraint', distro_constraint
  810. ])
  811. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  812. if tee_output:
  813. session.run('pylint', '--version')
  814. pylint_report_path = os.environ.get('PYLINT_REPORT')
  815. cmd_args = [
  816. 'pylint',
  817. '--rcfile={}'.format(rcfile)
  818. ] + list(flags) + list(paths)
  819. cmd_kwargs = {
  820. 'env': {'PYTHONUNBUFFERED': '1'}
  821. }
  822. if tee_output:
  823. stdout = tempfile.TemporaryFile(mode='w+b')
  824. cmd_kwargs['stdout'] = Tee(stdout, sys.__stdout__)
  825. lint_failed = False
  826. try:
  827. session.run(*cmd_args, **cmd_kwargs)
  828. except CommandFailed:
  829. lint_failed = True
  830. raise
  831. finally:
  832. if tee_output:
  833. stdout.seek(0)
  834. contents = stdout.read()
  835. if contents:
  836. if IS_PY3:
  837. contents = contents.decode('utf-8')
  838. else:
  839. contents = contents.encode('utf-8')
  840. sys.stdout.write(contents)
  841. sys.stdout.flush()
  842. if pylint_report_path:
  843. # Write report
  844. with open(pylint_report_path, 'w') as wfh:
  845. wfh.write(contents)
  846. session.log('Report file written to %r', pylint_report_path)
  847. stdout.close()
  848. def _lint_pre_commit(session, rcfile, flags, paths):
  849. if 'VIRTUAL_ENV' not in os.environ:
  850. session.error(
  851. 'This should be running from within a virtualenv and '
  852. '\'VIRTUAL_ENV\' was not found as an environment variable.'
  853. )
  854. if 'pre-commit' not in os.environ['VIRTUAL_ENV']:
  855. session.error(
  856. 'This should be running from within a pre-commit virtualenv and '
  857. '\'VIRTUAL_ENV\'({}) does not appear to be a pre-commit virtualenv.'.format(
  858. os.environ['VIRTUAL_ENV']
  859. )
  860. )
  861. from nox.virtualenv import VirtualEnv
  862. # Let's patch nox to make it run inside the pre-commit virtualenv
  863. try:
  864. session._runner.venv = VirtualEnv( # pylint: disable=unexpected-keyword-arg
  865. os.environ['VIRTUAL_ENV'],
  866. interpreter=session._runner.func.python,
  867. reuse_existing=True,
  868. venv=True
  869. )
  870. except TypeError:
  871. # This is still nox-py2
  872. session._runner.venv = VirtualEnv(
  873. os.environ['VIRTUAL_ENV'],
  874. interpreter=session._runner.func.python,
  875. reuse_existing=True,
  876. )
  877. _lint(session, rcfile, flags, paths, tee_output=False)
  878. @nox.session(python='3')
  879. def lint(session):
  880. '''
  881. Run PyLint against Salt and it's test suite. Set PYLINT_REPORT to a path to capture output.
  882. '''
  883. session.notify('lint-salt-{}'.format(session.python))
  884. session.notify('lint-tests-{}'.format(session.python))
  885. @nox.session(python='3', name='lint-salt')
  886. def lint_salt(session):
  887. '''
  888. Run PyLint against Salt. Set PYLINT_REPORT to a path to capture output.
  889. '''
  890. flags = [
  891. '--disable=I'
  892. ]
  893. if session.posargs:
  894. paths = session.posargs
  895. else:
  896. paths = ['setup.py', 'noxfile.py', 'salt/']
  897. _lint(session, '.pylintrc', flags, paths)
  898. @nox.session(python='3', name='lint-tests')
  899. def lint_tests(session):
  900. '''
  901. Run PyLint against Salt and it's test suite. Set PYLINT_REPORT to a path to capture output.
  902. '''
  903. flags = [
  904. '--disable=I'
  905. ]
  906. if session.posargs:
  907. paths = session.posargs
  908. else:
  909. paths = ['tests/']
  910. _lint(session, '.pylintrc', flags, paths)
  911. @nox.session(python=False, name='lint-salt-pre-commit')
  912. def lint_salt_pre_commit(session):
  913. '''
  914. Run PyLint against Salt. Set PYLINT_REPORT to a path to capture output.
  915. '''
  916. flags = [
  917. '--disable=I'
  918. ]
  919. if session.posargs:
  920. paths = session.posargs
  921. else:
  922. paths = ['setup.py', 'noxfile.py', 'salt/']
  923. _lint_pre_commit(session, '.pylintrc', flags, paths)
  924. @nox.session(python=False, name='lint-tests-pre-commit')
  925. def lint_tests_pre_commit(session):
  926. '''
  927. Run PyLint against Salt and it's test suite. Set PYLINT_REPORT to a path to capture output.
  928. '''
  929. flags = [
  930. '--disable=I'
  931. ]
  932. if session.posargs:
  933. paths = session.posargs
  934. else:
  935. paths = ['tests/']
  936. _lint_pre_commit(session, '.pylintrc', flags, paths)
  937. @nox.session(python='3')
  938. @nox.parametrize('update', [False, True])
  939. @nox.parametrize('compress', [False, True])
  940. def docs(session, compress, update):
  941. '''
  942. Build Salt's Documentation
  943. '''
  944. session.notify('docs-html(compress={})'.format(compress))
  945. session.notify('docs-man(compress={}, update={})'.format(compress, update))
  946. @nox.session(name='docs-html', python='3')
  947. @nox.parametrize('compress', [False, True])
  948. def docs_html(session, compress):
  949. '''
  950. Build Salt's HTML Documentation
  951. '''
  952. pydir = _get_pydir(session)
  953. if pydir == 'py3.4':
  954. session.error('Sphinx only runs on Python >= 3.5')
  955. requirements_file = 'requirements/static/docs.in'
  956. distro_constraints = [
  957. 'requirements/static/{}/docs.txt'.format(_get_pydir(session))
  958. ]
  959. install_command = [
  960. '--progress-bar=off', '-r', requirements_file
  961. ]
  962. for distro_constraint in distro_constraints:
  963. install_command.extend([
  964. '--constraint', distro_constraint
  965. ])
  966. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  967. os.chdir('doc/')
  968. session.run('make', 'clean', external=True)
  969. session.run('make', 'html', 'SPHINXOPTS=-W', external=True)
  970. if compress:
  971. session.run('tar', '-cJvf', 'html-archive.tar.xz', '_build/html', external=True)
  972. os.chdir('..')
  973. @nox.session(name='docs-man', python='3')
  974. @nox.parametrize('update', [False, True])
  975. @nox.parametrize('compress', [False, True])
  976. def docs_man(session, compress, update):
  977. '''
  978. Build Salt's Manpages Documentation
  979. '''
  980. pydir = _get_pydir(session)
  981. if pydir == 'py3.4':
  982. session.error('Sphinx only runs on Python >= 3.5')
  983. requirements_file = 'requirements/static/docs.in'
  984. distro_constraints = [
  985. 'requirements/static/{}/docs.txt'.format(_get_pydir(session))
  986. ]
  987. install_command = [
  988. '--progress-bar=off', '-r', requirements_file
  989. ]
  990. for distro_constraint in distro_constraints:
  991. install_command.extend([
  992. '--constraint', distro_constraint
  993. ])
  994. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  995. os.chdir('doc/')
  996. session.run('make', 'clean', external=True)
  997. session.run('make', 'man', 'SPHINXOPTS=-W', external=True)
  998. if update:
  999. session.run('rm', '-rf', 'man/', external=True)
  1000. session.run('cp', '-Rp', '_build/man', 'man/', external=True)
  1001. if compress:
  1002. session.run('tar', '-cJvf', 'man-archive.tar.xz', '_build/man', external=True)
  1003. os.chdir('..')