noxfile.py 41 KB

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