1
0

noxfile.py 41 KB

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