1
0

noxfile.py 41 KB

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