noxfile.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  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 json
  12. import os
  13. import pprint
  14. import shutil
  15. import sys
  16. import tempfile
  17. # fmt: off
  18. if __name__ == "__main__":
  19. sys.stderr.write(
  20. "Do not execute this file directly. Use nox instead, it will know how to handle this file\n"
  21. )
  22. sys.stderr.flush()
  23. exit(1)
  24. # fmt: on
  25. import nox # isort:skip
  26. from nox.command import CommandFailed # isort:skip
  27. IS_PY3 = sys.version_info > (2,)
  28. # Be verbose when runing under a CI context
  29. CI_RUN = (
  30. os.environ.get("JENKINS_URL")
  31. or os.environ.get("CI")
  32. or os.environ.get("DRONE") is not None
  33. )
  34. PIP_INSTALL_SILENT = CI_RUN is False
  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 = ("2", "2.7", "3", "3.4", "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 < (2, 7):
  109. session.error("Only Python >= 2.7 is supported")
  110. return "py{}.{}".format(*version_info)
  111. def _get_distro_info(session):
  112. try:
  113. distro = session._runner._distro
  114. except AttributeError:
  115. # The distro package doesn't output anything for Windows
  116. old_install_only_value = session._runner.global_config.install_only
  117. try:
  118. # Force install only to be false for the following chunk of code
  119. # For additional information as to why see:
  120. # https://github.com/theacodes/nox/pull/181
  121. session._runner.global_config.install_only = False
  122. session.install("--progress-bar=off", "distro", silent=PIP_INSTALL_SILENT)
  123. output = session.run("distro", "-j", silent=True)
  124. distro = json.loads(output.strip())
  125. session.log("Distro information:\n%s", pprint.pformat(distro))
  126. session._runner._distro = distro
  127. finally:
  128. session._runner.global_config.install_only = old_install_only_value
  129. return distro
  130. def _install_system_packages(session):
  131. """
  132. Because some python packages are provided by the distribution and cannot
  133. be pip installed, and because we don't want the whole system python packages
  134. on our virtualenvs, we copy the required system python packages into
  135. the virtualenv
  136. """
  137. system_python_packages = {
  138. "__debian_based_distros__": ["/usr/lib/python{py_version}/dist-packages/*apt*"]
  139. }
  140. distro = _get_distro_info(session)
  141. if not distro["id"].startswith(("debian", "ubuntu")):
  142. # This only applies to debian based distributions
  143. return
  144. system_python_packages["{id}-{version}".format(**distro)] = system_python_packages[
  145. "{id}-{version_parts[major]}".format(**distro)
  146. ] = system_python_packages["__debian_based_distros__"][:]
  147. distro_keys = [
  148. "{id}".format(**distro),
  149. "{id}-{version}".format(**distro),
  150. "{id}-{version_parts[major]}".format(**distro),
  151. ]
  152. version_info = _get_session_python_version_info(session)
  153. py_version_keys = ["{}".format(*version_info), "{}.{}".format(*version_info)]
  154. session_site_packages_dir = _get_session_python_site_packages_dir(session)
  155. for distro_key in distro_keys:
  156. if distro_key not in system_python_packages:
  157. continue
  158. patterns = system_python_packages[distro_key]
  159. for pattern in patterns:
  160. for py_version in py_version_keys:
  161. matches = set(glob.glob(pattern.format(py_version=py_version)))
  162. if not matches:
  163. continue
  164. for match in matches:
  165. src = os.path.realpath(match)
  166. dst = os.path.join(
  167. session_site_packages_dir, os.path.basename(match)
  168. )
  169. if os.path.exists(dst):
  170. session.log(
  171. "Not overwritting already existing %s with %s", dst, src
  172. )
  173. continue
  174. session.log("Copying %s into %s", src, dst)
  175. if os.path.isdir(src):
  176. shutil.copytree(src, dst)
  177. else:
  178. shutil.copyfile(src, dst)
  179. def _get_distro_pip_constraints(session, transport):
  180. # Install requirements
  181. distro_constraints = []
  182. if transport == "tcp":
  183. # The TCP requirements are the exact same requirements as the ZeroMQ ones
  184. transport = "zeromq"
  185. pydir = _get_pydir(session)
  186. if IS_WINDOWS:
  187. _distro_constraints = os.path.join(
  188. "requirements", "static", pydir, "{}-windows.txt".format(transport)
  189. )
  190. if os.path.exists(_distro_constraints):
  191. distro_constraints.append(_distro_constraints)
  192. _distro_constraints = os.path.join(
  193. "requirements", "static", pydir, "windows.txt"
  194. )
  195. if os.path.exists(_distro_constraints):
  196. distro_constraints.append(_distro_constraints)
  197. _distro_constraints = os.path.join(
  198. "requirements", "static", pydir, "windows-crypto.txt"
  199. )
  200. if os.path.exists(_distro_constraints):
  201. distro_constraints.append(_distro_constraints)
  202. elif IS_DARWIN:
  203. _distro_constraints = os.path.join(
  204. "requirements", "static", pydir, "{}-darwin.txt".format(transport)
  205. )
  206. if os.path.exists(_distro_constraints):
  207. distro_constraints.append(_distro_constraints)
  208. _distro_constraints = os.path.join(
  209. "requirements", "static", pydir, "darwin.txt"
  210. )
  211. if os.path.exists(_distro_constraints):
  212. distro_constraints.append(_distro_constraints)
  213. _distro_constraints = os.path.join(
  214. "requirements", "static", pydir, "darwin-crypto.txt"
  215. )
  216. if os.path.exists(_distro_constraints):
  217. distro_constraints.append(_distro_constraints)
  218. else:
  219. _install_system_packages(session)
  220. distro = _get_distro_info(session)
  221. distro_keys = [
  222. "linux",
  223. "{id}".format(**distro),
  224. "{id}-{version}".format(**distro),
  225. "{id}-{version_parts[major]}".format(**distro),
  226. ]
  227. for distro_key in distro_keys:
  228. _distro_constraints = os.path.join(
  229. "requirements", "static", pydir, "{}.txt".format(distro_key)
  230. )
  231. if os.path.exists(_distro_constraints):
  232. distro_constraints.append(_distro_constraints)
  233. _distro_constraints = os.path.join(
  234. "requirements", "static", pydir, "{}-crypto.txt".format(distro_key)
  235. )
  236. if os.path.exists(_distro_constraints):
  237. distro_constraints.append(_distro_constraints)
  238. _distro_constraints = os.path.join(
  239. "requirements",
  240. "static",
  241. pydir,
  242. "{}-{}.txt".format(transport, distro_key),
  243. )
  244. if os.path.exists(_distro_constraints):
  245. distro_constraints.append(_distro_constraints)
  246. distro_constraints.append(_distro_constraints)
  247. _distro_constraints = os.path.join(
  248. "requirements",
  249. "static",
  250. pydir,
  251. "{}-{}-crypto.txt".format(transport, distro_key),
  252. )
  253. if os.path.exists(_distro_constraints):
  254. distro_constraints.append(_distro_constraints)
  255. return distro_constraints
  256. def _install_requirements(session, transport, *extra_requirements):
  257. # Install requirements
  258. distro_constraints = _get_distro_pip_constraints(session, transport)
  259. _requirements_files = [
  260. os.path.join("requirements", "base.txt"),
  261. os.path.join("requirements", "zeromq.txt"),
  262. os.path.join("requirements", "pytest.txt"),
  263. ]
  264. if sys.platform.startswith("linux"):
  265. requirements_files = [os.path.join("requirements", "static", "linux.in")]
  266. elif sys.platform.startswith("win"):
  267. requirements_files = [
  268. os.path.join("pkg", "windows", "req.txt"),
  269. os.path.join("requirements", "static", "windows.in"),
  270. ]
  271. elif sys.platform.startswith("darwin"):
  272. requirements_files = [
  273. os.path.join("pkg", "osx", "req.txt"),
  274. os.path.join("pkg", "osx", "req_ext.txt"),
  275. os.path.join("pkg", "osx", "req_pyobjc.txt"),
  276. os.path.join("requirements", "static", "darwin.in"),
  277. ]
  278. while True:
  279. if not requirements_files:
  280. break
  281. requirements_file = requirements_files.pop(0)
  282. if requirements_file not in _requirements_files:
  283. _requirements_files.append(requirements_file)
  284. session.log("Processing {}".format(requirements_file))
  285. with open(requirements_file) as rfh: # pylint: disable=resource-leakage
  286. for line in rfh:
  287. line = line.strip()
  288. if not line:
  289. continue
  290. if line.startswith("-r"):
  291. reqfile = os.path.join(
  292. os.path.dirname(requirements_file), line.strip().split()[-1]
  293. )
  294. if reqfile in _requirements_files:
  295. continue
  296. _requirements_files.append(reqfile)
  297. continue
  298. for requirements_file in _requirements_files:
  299. install_command = ["--progress-bar=off", "-r", requirements_file]
  300. for distro_constraint in distro_constraints:
  301. install_command.extend(["--constraint", distro_constraint])
  302. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  303. if extra_requirements:
  304. install_command = [
  305. "--progress-bar=off",
  306. ]
  307. for distro_constraint in distro_constraints:
  308. install_command.extend(["--constraint", distro_constraint])
  309. install_command += list(extra_requirements)
  310. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  311. def _run_with_coverage(session, *test_cmd):
  312. session.install("--progress-bar=off", "coverage==5.0.1", silent=PIP_INSTALL_SILENT)
  313. session.run("coverage", "erase")
  314. python_path_env_var = os.environ.get("PYTHONPATH") or None
  315. if python_path_env_var is None:
  316. python_path_env_var = SITECUSTOMIZE_DIR
  317. else:
  318. python_path_entries = python_path_env_var.split(os.pathsep)
  319. if SITECUSTOMIZE_DIR in python_path_entries:
  320. python_path_entries.remove(SITECUSTOMIZE_DIR)
  321. python_path_entries.insert(0, SITECUSTOMIZE_DIR)
  322. python_path_env_var = os.pathsep.join(python_path_entries)
  323. env = {
  324. # The updated python path so that sitecustomize is importable
  325. "PYTHONPATH": python_path_env_var,
  326. # The full path to the .coverage data file. Makes sure we always write
  327. # them to the same directory
  328. "COVERAGE_FILE": os.path.abspath(os.path.join(REPO_ROOT, ".coverage")),
  329. # Instruct sub processes to also run under coverage
  330. "COVERAGE_PROCESS_START": os.path.join(REPO_ROOT, ".coveragerc"),
  331. }
  332. if IS_DARWIN:
  333. # Don't nuke our multiprocessing efforts objc!
  334. # https://stackoverflow.com/questions/50168647/multiprocessing-causes-python-to-crash-and-gives-an-error-may-have-been-in-progr
  335. env["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
  336. try:
  337. session.run(*test_cmd, env=env)
  338. finally:
  339. # Always combine and generate the XML coverage report
  340. try:
  341. session.run("coverage", "combine")
  342. except CommandFailed:
  343. # Sometimes some of the coverage files are corrupt which would trigger a CommandFailed
  344. # exception
  345. pass
  346. # Generate report for salt code coverage
  347. session.run(
  348. "coverage",
  349. "xml",
  350. "-o",
  351. os.path.join("artifacts", "coverage", "salt.xml"),
  352. "--omit=tests/*",
  353. "--include=salt/*",
  354. )
  355. # Generate report for tests code coverage
  356. session.run(
  357. "coverage",
  358. "xml",
  359. "-o",
  360. os.path.join("artifacts", "coverage", "tests.xml"),
  361. "--omit=salt/*",
  362. "--include=tests/*",
  363. )
  364. @nox.session(python=_PYTHON_VERSIONS, name="pytest-parametrized")
  365. @nox.parametrize("coverage", [False, True])
  366. @nox.parametrize("transport", ["zeromq", "tcp"])
  367. @nox.parametrize("crypto", [None, "m2crypto", "pycryptodome"])
  368. def pytest_parametrized(session, coverage, transport, crypto):
  369. # Install requirements
  370. _install_requirements(session, transport)
  371. if crypto:
  372. session.run(
  373. "pip",
  374. "uninstall",
  375. "-y",
  376. "m2crypto",
  377. "pycrypto",
  378. "pycryptodome",
  379. "pycryptodomex",
  380. silent=True,
  381. )
  382. distro_constraints = _get_distro_pip_constraints(session, transport)
  383. install_command = [
  384. "--progress-bar=off",
  385. ]
  386. for distro_constraint in distro_constraints:
  387. install_command.extend(["--constraint", distro_constraint])
  388. install_command.append(crypto)
  389. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  390. cmd_args = [
  391. "--rootdir",
  392. REPO_ROOT,
  393. "--log-file={}".format(RUNTESTS_LOGFILE),
  394. "--log-file-level=debug",
  395. "--no-print-logs",
  396. "-ra",
  397. "-s",
  398. "--transport={}".format(transport),
  399. ] + session.posargs
  400. _pytest(session, coverage, cmd_args)
  401. @nox.session(python=_PYTHON_VERSIONS)
  402. @nox.parametrize("coverage", [False, True])
  403. def pytest(session, coverage):
  404. """
  405. pytest session with zeromq transport and default crypto
  406. """
  407. session.notify(
  408. "pytest-parametrized-{}(coverage={}, crypto=None, transport='zeromq')".format(
  409. session.python, coverage
  410. )
  411. )
  412. @nox.session(python=_PYTHON_VERSIONS, name="pytest-tcp")
  413. @nox.parametrize("coverage", [False, True])
  414. def pytest_tcp(session, coverage):
  415. """
  416. pytest session with TCP transport and default crypto
  417. """
  418. session.notify(
  419. "pytest-parametrized-{}(coverage={}, crypto=None, transport='tcp')".format(
  420. session.python, coverage
  421. )
  422. )
  423. @nox.session(python=_PYTHON_VERSIONS, name="pytest-zeromq")
  424. @nox.parametrize("coverage", [False, True])
  425. def pytest_zeromq(session, coverage):
  426. """
  427. pytest session with zeromq transport and default crypto
  428. """
  429. session.notify(
  430. "pytest-parametrized-{}(coverage={}, crypto=None, transport='zeromq')".format(
  431. session.python, coverage
  432. )
  433. )
  434. @nox.session(python=_PYTHON_VERSIONS, name="pytest-m2crypto")
  435. @nox.parametrize("coverage", [False, True])
  436. def pytest_m2crypto(session, coverage):
  437. """
  438. pytest session with zeromq transport and m2crypto
  439. """
  440. session.notify(
  441. "pytest-parametrized-{}(coverage={}, crypto='m2crypto', transport='zeromq')".format(
  442. session.python, coverage
  443. )
  444. )
  445. @nox.session(python=_PYTHON_VERSIONS, name="pytest-tcp-m2crypto")
  446. @nox.parametrize("coverage", [False, True])
  447. def pytest_tcp_m2crypto(session, coverage):
  448. """
  449. pytest session with TCP transport and m2crypto
  450. """
  451. session.notify(
  452. "pytest-parametrized-{}(coverage={}, crypto='m2crypto', transport='tcp')".format(
  453. session.python, coverage
  454. )
  455. )
  456. @nox.session(python=_PYTHON_VERSIONS, name="pytest-zeromq-m2crypto")
  457. @nox.parametrize("coverage", [False, True])
  458. def pytest_zeromq_m2crypto(session, coverage):
  459. """
  460. pytest session with zeromq transport and m2crypto
  461. """
  462. session.notify(
  463. "pytest-parametrized-{}(coverage={}, crypto='m2crypto', transport='zeromq')".format(
  464. session.python, coverage
  465. )
  466. )
  467. @nox.session(python=_PYTHON_VERSIONS, name="pytest-pycryptodome")
  468. @nox.parametrize("coverage", [False, True])
  469. def pytest_pycryptodome(session, coverage):
  470. """
  471. pytest session with zeromq transport and pycryptodome
  472. """
  473. session.notify(
  474. "pytest-parametrized-{}(coverage={}, crypto='pycryptodome', transport='zeromq')".format(
  475. session.python, coverage
  476. )
  477. )
  478. @nox.session(python=_PYTHON_VERSIONS, name="pytest-tcp-pycryptodome")
  479. @nox.parametrize("coverage", [False, True])
  480. def pytest_tcp_pycryptodome(session, coverage):
  481. """
  482. pytest session with TCP transport and pycryptodome
  483. """
  484. session.notify(
  485. "pytest-parametrized-{}(coverage={}, crypto='pycryptodome', transport='tcp')".format(
  486. session.python, coverage
  487. )
  488. )
  489. @nox.session(python=_PYTHON_VERSIONS, name="pytest-zeromq-pycryptodome")
  490. @nox.parametrize("coverage", [False, True])
  491. def pytest_zeromq_pycryptodome(session, coverage):
  492. """
  493. pytest session with zeromq transport and pycryptodome
  494. """
  495. session.notify(
  496. "pytest-parametrized-{}(coverage={}, crypto='pycryptodome', transport='zeromq')".format(
  497. session.python, coverage
  498. )
  499. )
  500. @nox.session(python=_PYTHON_VERSIONS, name="pytest-cloud")
  501. @nox.parametrize("coverage", [False, True])
  502. def pytest_cloud(session, coverage):
  503. # Install requirements
  504. _install_requirements(session, "zeromq")
  505. pydir = _get_pydir(session)
  506. cloud_requirements = os.path.join("requirements", "static", pydir, "cloud.txt")
  507. session.install(
  508. "--progress-bar=off", "-r", cloud_requirements, silent=PIP_INSTALL_SILENT
  509. )
  510. cmd_args = [
  511. "--rootdir",
  512. REPO_ROOT,
  513. "--log-file={}".format(RUNTESTS_LOGFILE),
  514. "--log-file-level=debug",
  515. "--no-print-logs",
  516. "-ra",
  517. "-s",
  518. "--run-expensive",
  519. "-k",
  520. "cloud",
  521. ] + session.posargs
  522. _pytest(session, coverage, cmd_args)
  523. @nox.session(python=_PYTHON_VERSIONS, name="pytest-tornado")
  524. @nox.parametrize("coverage", [False, True])
  525. def pytest_tornado(session, coverage):
  526. # Install requirements
  527. _install_requirements(session, "zeromq")
  528. session.install("--progress-bar=off", "tornado==5.0.2", silent=PIP_INSTALL_SILENT)
  529. session.install("--progress-bar=off", "pyzmq==17.0.0", silent=PIP_INSTALL_SILENT)
  530. cmd_args = [
  531. "--rootdir",
  532. REPO_ROOT,
  533. "--log-file={}".format(RUNTESTS_LOGFILE),
  534. "--log-file-level=debug",
  535. "--no-print-logs",
  536. "-ra",
  537. "-s",
  538. ] + session.posargs
  539. _pytest(session, coverage, cmd_args)
  540. def _pytest(session, coverage, cmd_args):
  541. # Create required artifacts directories
  542. _create_ci_directories()
  543. env = None
  544. if IS_DARWIN:
  545. # Don't nuke our multiprocessing efforts objc!
  546. # https://stackoverflow.com/questions/50168647/multiprocessing-causes-python-to-crash-and-gives-an-error-may-have-been-in-progr
  547. env = {"OBJC_DISABLE_INITIALIZE_FORK_SAFETY": "YES"}
  548. if CI_RUN:
  549. # We'll print out the collected tests on CI runs.
  550. # This will show a full list of what tests are going to run, in the right order, which, in case
  551. # of a test suite hang, helps us pinpoint which test is hanging
  552. session.run(
  553. "python", "-m", "pytest", *(cmd_args + ["--collect-only", "-qqq"]), env=env
  554. )
  555. try:
  556. if coverage is True:
  557. _run_with_coverage(
  558. session, "python", "-m", "coverage", "run", "-m", "pytest", *cmd_args
  559. )
  560. else:
  561. session.run("python", "-m", "pytest", *cmd_args, env=env)
  562. except CommandFailed: # pylint: disable=try-except-raise
  563. # Not rerunning failed tests for now
  564. raise
  565. # pylint: disable=unreachable
  566. # Re-run failed tests
  567. session.log("Re-running failed tests")
  568. for idx, parg in enumerate(cmd_args):
  569. if parg.startswith("--junitxml="):
  570. cmd_args[idx] = parg.replace(".xml", "-rerun-failed.xml")
  571. cmd_args.append("--lf")
  572. if coverage is True:
  573. _run_with_coverage(
  574. session, "python", "-m", "coverage", "run", "-m", "pytest", *cmd_args
  575. )
  576. else:
  577. session.run("python", "-m", "pytest", *cmd_args, env=env)
  578. # pylint: enable=unreachable
  579. class Tee:
  580. """
  581. Python class to mimic linux tee behaviour
  582. """
  583. def __init__(self, first, second):
  584. self._first = first
  585. self._second = second
  586. def write(self, b):
  587. wrote = self._first.write(b)
  588. self._first.flush()
  589. self._second.write(b)
  590. self._second.flush()
  591. def fileno(self):
  592. return self._first.fileno()
  593. def _lint(session, rcfile, flags, paths, tee_output=True):
  594. _install_requirements(session, "zeromq")
  595. requirements_file = "requirements/static/lint.in"
  596. distro_constraints = ["requirements/static/{}/lint.txt".format(_get_pydir(session))]
  597. install_command = ["--progress-bar=off", "-r", requirements_file]
  598. for distro_constraint in distro_constraints:
  599. install_command.extend(["--constraint", distro_constraint])
  600. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  601. if tee_output:
  602. session.run("pylint", "--version")
  603. pylint_report_path = os.environ.get("PYLINT_REPORT")
  604. cmd_args = ["pylint", "--rcfile={}".format(rcfile)] + list(flags) + list(paths)
  605. cmd_kwargs = {"env": {"PYTHONUNBUFFERED": "1"}}
  606. if tee_output:
  607. stdout = tempfile.TemporaryFile(mode="w+b")
  608. cmd_kwargs["stdout"] = Tee(stdout, sys.__stdout__)
  609. lint_failed = False
  610. try:
  611. session.run(*cmd_args, **cmd_kwargs)
  612. except CommandFailed:
  613. lint_failed = True
  614. raise
  615. finally:
  616. if tee_output:
  617. stdout.seek(0)
  618. contents = stdout.read()
  619. if contents:
  620. if IS_PY3:
  621. contents = contents.decode("utf-8")
  622. else:
  623. contents = contents.encode("utf-8")
  624. sys.stdout.write(contents)
  625. sys.stdout.flush()
  626. if pylint_report_path:
  627. # Write report
  628. with open(pylint_report_path, "w") as wfh:
  629. wfh.write(contents)
  630. session.log("Report file written to %r", pylint_report_path)
  631. stdout.close()
  632. def _lint_pre_commit(session, rcfile, flags, paths):
  633. if "VIRTUAL_ENV" not in os.environ:
  634. session.error(
  635. "This should be running from within a virtualenv and "
  636. "'VIRTUAL_ENV' was not found as an environment variable."
  637. )
  638. if "pre-commit" not in os.environ["VIRTUAL_ENV"]:
  639. session.error(
  640. "This should be running from within a pre-commit virtualenv and "
  641. "'VIRTUAL_ENV'({}) does not appear to be a pre-commit virtualenv.".format(
  642. os.environ["VIRTUAL_ENV"]
  643. )
  644. )
  645. from nox.virtualenv import VirtualEnv
  646. # Let's patch nox to make it run inside the pre-commit virtualenv
  647. try:
  648. session._runner.venv = VirtualEnv( # pylint: disable=unexpected-keyword-arg
  649. os.environ["VIRTUAL_ENV"],
  650. interpreter=session._runner.func.python,
  651. reuse_existing=True,
  652. venv=True,
  653. )
  654. except TypeError:
  655. # This is still nox-py2
  656. session._runner.venv = VirtualEnv(
  657. os.environ["VIRTUAL_ENV"],
  658. interpreter=session._runner.func.python,
  659. reuse_existing=True,
  660. )
  661. _lint(session, rcfile, flags, paths, tee_output=False)
  662. @nox.session(python="3")
  663. def lint(session):
  664. """
  665. Run PyLint against Salt and it's test suite. Set PYLINT_REPORT to a path to capture output.
  666. """
  667. session.notify("lint-salt-{}".format(session.python))
  668. session.notify("lint-tests-{}".format(session.python))
  669. @nox.session(python="3", name="lint-salt")
  670. def lint_salt(session):
  671. """
  672. Run PyLint against Salt. Set PYLINT_REPORT to a path to capture output.
  673. """
  674. flags = ["--disable=I"]
  675. if session.posargs:
  676. paths = session.posargs
  677. else:
  678. paths = ["setup.py", "noxfile.py", "salt/", "tasks/"]
  679. _lint(session, ".pylintrc", flags, paths)
  680. @nox.session(python="3", name="lint-tests")
  681. def lint_tests(session):
  682. """
  683. Run PyLint against Salt and it's test suite. Set PYLINT_REPORT to a path to capture output.
  684. """
  685. flags = ["--disable=I"]
  686. if session.posargs:
  687. paths = session.posargs
  688. else:
  689. paths = ["tests/"]
  690. _lint(session, ".pylintrc", flags, paths)
  691. @nox.session(python=False, name="lint-salt-pre-commit")
  692. def lint_salt_pre_commit(session):
  693. """
  694. Run PyLint against Salt. Set PYLINT_REPORT to a path to capture output.
  695. """
  696. flags = ["--disable=I"]
  697. if session.posargs:
  698. paths = session.posargs
  699. else:
  700. paths = ["setup.py", "noxfile.py", "salt/"]
  701. _lint_pre_commit(session, ".pylintrc", flags, paths)
  702. @nox.session(python=False, name="lint-tests-pre-commit")
  703. def lint_tests_pre_commit(session):
  704. """
  705. Run PyLint against Salt and it's test suite. Set PYLINT_REPORT to a path to capture output.
  706. """
  707. flags = ["--disable=I"]
  708. if session.posargs:
  709. paths = session.posargs
  710. else:
  711. paths = ["tests/"]
  712. _lint_pre_commit(session, ".pylintrc", flags, paths)
  713. @nox.session(python="3")
  714. @nox.parametrize("update", [False, True])
  715. @nox.parametrize("compress", [False, True])
  716. def docs(session, compress, update):
  717. """
  718. Build Salt's Documentation
  719. """
  720. session.notify("docs-html(compress={})".format(compress))
  721. session.notify("docs-man(compress={}, update={})".format(compress, update))
  722. @nox.session(name="docs-html", python="3")
  723. @nox.parametrize("compress", [False, True])
  724. def docs_html(session, compress):
  725. """
  726. Build Salt's HTML Documentation
  727. """
  728. pydir = _get_pydir(session)
  729. if pydir == "py3.4":
  730. session.error("Sphinx only runs on Python >= 3.5")
  731. requirements_file = "requirements/static/docs.in"
  732. distro_constraints = ["requirements/static/{}/docs.txt".format(_get_pydir(session))]
  733. install_command = ["--progress-bar=off", "-r", requirements_file]
  734. for distro_constraint in distro_constraints:
  735. install_command.extend(["--constraint", distro_constraint])
  736. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  737. os.chdir("doc/")
  738. session.run("make", "clean", external=True)
  739. session.run("make", "html", "SPHINXOPTS=-W", external=True)
  740. if compress:
  741. session.run("tar", "-cJvf", "html-archive.tar.xz", "_build/html", external=True)
  742. os.chdir("..")
  743. @nox.session(name="docs-man", python="3")
  744. @nox.parametrize("update", [False, True])
  745. @nox.parametrize("compress", [False, True])
  746. def docs_man(session, compress, update):
  747. """
  748. Build Salt's Manpages Documentation
  749. """
  750. pydir = _get_pydir(session)
  751. if pydir == "py3.4":
  752. session.error("Sphinx only runs on Python >= 3.5")
  753. requirements_file = "requirements/static/docs.in"
  754. distro_constraints = ["requirements/static/{}/docs.txt".format(_get_pydir(session))]
  755. install_command = ["--progress-bar=off", "-r", requirements_file]
  756. for distro_constraint in distro_constraints:
  757. install_command.extend(["--constraint", distro_constraint])
  758. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  759. os.chdir("doc/")
  760. session.run("make", "clean", external=True)
  761. session.run("make", "man", "SPHINXOPTS=-W", external=True)
  762. if update:
  763. session.run("rm", "-rf", "man/", external=True)
  764. session.run("cp", "-Rp", "_build/man", "man/", external=True)
  765. if compress:
  766. session.run("tar", "-cJvf", "man-archive.tar.xz", "_build/man", external=True)
  767. os.chdir("..")
  768. def _invoke(session):
  769. """
  770. Run invoke tasks
  771. """
  772. requirements_file = "requirements/static/invoke.in"
  773. distro_constraints = [
  774. "requirements/static/{}/invoke.txt".format(_get_pydir(session))
  775. ]
  776. install_command = ["--progress-bar=off", "-r", requirements_file]
  777. for distro_constraint in distro_constraints:
  778. install_command.extend(["--constraint", distro_constraint])
  779. session.install(*install_command, silent=PIP_INSTALL_SILENT)
  780. cmd = ["inv"]
  781. files = []
  782. # Unfortunately, invoke doesn't support the nargs functionality like argpase does.
  783. # Let's make it behave properly
  784. for idx, posarg in enumerate(session.posargs):
  785. if idx == 0:
  786. cmd.append(posarg)
  787. continue
  788. if posarg.startswith("--"):
  789. cmd.append(posarg)
  790. continue
  791. files.append(posarg)
  792. if files:
  793. cmd.append("--files={}".format(" ".join(files)))
  794. session.run(*cmd)
  795. @nox.session(name="invoke", python="3")
  796. def invoke(session):
  797. _invoke(session)
  798. @nox.session(name="invoke-pre-commit", python="3")
  799. def invoke_pre_commit(session):
  800. if "VIRTUAL_ENV" not in os.environ:
  801. session.error(
  802. "This should be running from within a virtualenv and "
  803. "'VIRTUAL_ENV' was not found as an environment variable."
  804. )
  805. if "pre-commit" not in os.environ["VIRTUAL_ENV"]:
  806. session.error(
  807. "This should be running from within a pre-commit virtualenv and "
  808. "'VIRTUAL_ENV'({}) does not appear to be a pre-commit virtualenv.".format(
  809. os.environ["VIRTUAL_ENV"]
  810. )
  811. )
  812. from nox.virtualenv import VirtualEnv
  813. # Let's patch nox to make it run inside the pre-commit virtualenv
  814. try:
  815. session._runner.venv = VirtualEnv( # pylint: disable=unexpected-keyword-arg
  816. os.environ["VIRTUAL_ENV"],
  817. interpreter=session._runner.func.python,
  818. reuse_existing=True,
  819. venv=True,
  820. )
  821. except TypeError:
  822. # This is still nox-py2
  823. session._runner.venv = VirtualEnv(
  824. os.environ["VIRTUAL_ENV"],
  825. interpreter=session._runner.func.python,
  826. reuse_existing=True,
  827. )
  828. _invoke(session)