1
0

noxfile.py 38 KB

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