noxfile.py 41 KB

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