1
0

noxfile.py 42 KB

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