noxfile.py 37 KB

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