test_pip_state.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  4. tests.unit.states.pip_test
  5. ~~~~~~~~~~~~~~~~~~~~~~~~~~
  6. """
  7. # Import python libs
  8. from __future__ import absolute_import, print_function, unicode_literals
  9. import logging
  10. import os
  11. import subprocess
  12. import sys
  13. import pytest
  14. import salt.states.pip_state as pip_state
  15. import salt.utils.path
  16. # Import salt libs
  17. import salt.version
  18. from salt.modules.virtualenv_mod import KNOWN_BINARY_NAMES
  19. from tests.support.helpers import VirtualEnv, dedent
  20. # Import Salt Testing libs
  21. from tests.support.mixins import LoaderModuleMockMixin, SaltReturnAssertsMixin
  22. from tests.support.mock import MagicMock, patch
  23. from tests.support.runtests import RUNTIME_VARS
  24. from tests.support.unit import TestCase, skipIf
  25. # Import 3rd-party libs
  26. try:
  27. import pip
  28. HAS_PIP = True
  29. except ImportError:
  30. HAS_PIP = False
  31. log = logging.getLogger(__name__)
  32. @skipIf(not HAS_PIP, "The 'pip' library is not importable(installed system-wide)")
  33. class PipStateTest(TestCase, SaltReturnAssertsMixin, LoaderModuleMockMixin):
  34. def setup_loader_modules(self):
  35. return {
  36. pip_state: {
  37. "__env__": "base",
  38. "__opts__": {"test": False},
  39. "__salt__": {"cmd.which_bin": lambda _: "pip"},
  40. }
  41. }
  42. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  43. def test_install_requirements_parsing(self):
  44. log.debug("Real pip version is %s", pip.__version__)
  45. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  46. pip_list = MagicMock(return_value={"pep8": "1.3.3"})
  47. pip_version = pip.__version__
  48. mock_pip_version = MagicMock(return_value=pip_version)
  49. with patch.dict(pip_state.__salt__, {"pip.version": mock_pip_version}):
  50. with patch.dict(
  51. pip_state.__salt__, {"cmd.run_all": mock, "pip.list": pip_list}
  52. ):
  53. with patch.dict(pip_state.__opts__, {"test": True}):
  54. log.debug(
  55. "pip_state._from_line globals: %s",
  56. [name for name in pip_state._from_line.__globals__],
  57. )
  58. ret = pip_state.installed("pep8=1.3.2")
  59. self.assertSaltFalseReturn({"test": ret})
  60. self.assertInSaltComment(
  61. "Invalid version specification in package pep8=1.3.2. "
  62. "'=' is not supported, use '==' instead.",
  63. {"test": ret},
  64. )
  65. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  66. pip_list = MagicMock(return_value={"pep8": "1.3.3"})
  67. pip_install = MagicMock(return_value={"retcode": 0})
  68. with patch.dict(
  69. pip_state.__salt__,
  70. {"cmd.run_all": mock, "pip.list": pip_list, "pip.install": pip_install},
  71. ):
  72. with patch.dict(pip_state.__opts__, {"test": True}):
  73. ret = pip_state.installed("pep8>=1.3.2")
  74. self.assertSaltTrueReturn({"test": ret})
  75. self.assertInSaltComment(
  76. "Python package pep8>=1.3.2 was already installed",
  77. {"test": ret},
  78. )
  79. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  80. pip_list = MagicMock(return_value={"pep8": "1.3.3"})
  81. with patch.dict(
  82. pip_state.__salt__, {"cmd.run_all": mock, "pip.list": pip_list}
  83. ):
  84. with patch.dict(pip_state.__opts__, {"test": True}):
  85. ret = pip_state.installed("pep8<1.3.2")
  86. self.assertSaltNoneReturn({"test": ret})
  87. self.assertInSaltComment(
  88. "Python package pep8<1.3.2 is set to be installed",
  89. {"test": ret},
  90. )
  91. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  92. pip_list = MagicMock(return_value={"pep8": "1.3.2"})
  93. pip_install = MagicMock(return_value={"retcode": 0})
  94. with patch.dict(
  95. pip_state.__salt__,
  96. {"cmd.run_all": mock, "pip.list": pip_list, "pip.install": pip_install},
  97. ):
  98. with patch.dict(pip_state.__opts__, {"test": True}):
  99. ret = pip_state.installed("pep8>1.3.1,<1.3.3")
  100. self.assertSaltTrueReturn({"test": ret})
  101. self.assertInSaltComment(
  102. "Python package pep8>1.3.1,<1.3.3 was already installed",
  103. {"test": ret},
  104. )
  105. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  106. pip_list = MagicMock(return_value={"pep8": "1.3.1"})
  107. pip_install = MagicMock(return_value={"retcode": 0})
  108. with patch.dict(
  109. pip_state.__salt__,
  110. {"cmd.run_all": mock, "pip.list": pip_list, "pip.install": pip_install},
  111. ):
  112. with patch.dict(pip_state.__opts__, {"test": True}):
  113. ret = pip_state.installed("pep8>1.3.1,<1.3.3")
  114. self.assertSaltNoneReturn({"test": ret})
  115. self.assertInSaltComment(
  116. "Python package pep8>1.3.1,<1.3.3 is set to be installed",
  117. {"test": ret},
  118. )
  119. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  120. pip_list = MagicMock(return_value={"pep8": "1.3.1"})
  121. with patch.dict(
  122. pip_state.__salt__, {"cmd.run_all": mock, "pip.list": pip_list}
  123. ):
  124. with patch.dict(pip_state.__opts__, {"test": True}):
  125. ret = pip_state.installed(
  126. "git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting>=0.5.1"
  127. )
  128. self.assertSaltNoneReturn({"test": ret})
  129. self.assertInSaltComment(
  130. "Python package git+https://github.com/saltstack/"
  131. "salt-testing.git#egg=SaltTesting>=0.5.1 is set to be "
  132. "installed",
  133. {"test": ret},
  134. )
  135. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  136. pip_list = MagicMock(return_value={"pep8": "1.3.1"})
  137. with patch.dict(
  138. pip_state.__salt__, {"cmd.run_all": mock, "pip.list": pip_list}
  139. ):
  140. with patch.dict(pip_state.__opts__, {"test": True}):
  141. ret = pip_state.installed(
  142. "git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting"
  143. )
  144. self.assertSaltNoneReturn({"test": ret})
  145. self.assertInSaltComment(
  146. "Python package git+https://github.com/saltstack/"
  147. "salt-testing.git#egg=SaltTesting is set to be "
  148. "installed",
  149. {"test": ret},
  150. )
  151. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  152. pip_list = MagicMock(return_value={"pep8": "1.3.1"})
  153. with patch.dict(
  154. pip_state.__salt__, {"cmd.run_all": mock, "pip.list": pip_list}
  155. ):
  156. with patch.dict(pip_state.__opts__, {"test": True}):
  157. ret = pip_state.installed(
  158. "https://pypi.python.org/packages/source/S/SaltTesting/"
  159. "SaltTesting-0.5.0.tar.gz"
  160. "#md5=e6760af92b7165f8be53b5763e40bc24"
  161. )
  162. self.assertSaltNoneReturn({"test": ret})
  163. self.assertInSaltComment(
  164. "Python package https://pypi.python.org/packages/source/"
  165. "S/SaltTesting/SaltTesting-0.5.0.tar.gz"
  166. "#md5=e6760af92b7165f8be53b5763e40bc24 is set to be "
  167. "installed",
  168. {"test": ret},
  169. )
  170. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  171. pip_list = MagicMock(return_value={"SaltTesting": "0.5.0"})
  172. pip_install = MagicMock(
  173. return_value={
  174. "retcode": 0,
  175. "stderr": "",
  176. "stdout": "Downloading/unpacking https://pypi.python.org/packages"
  177. "/source/S/SaltTesting/SaltTesting-0.5.0.tar.gz\n "
  178. "Downloading SaltTesting-0.5.0.tar.gz\n Running "
  179. "setup.py egg_info for package from "
  180. "https://pypi.python.org/packages/source/S/SaltTesting/"
  181. "SaltTesting-0.5.0.tar.gz\n \nCleaning up...",
  182. }
  183. )
  184. with patch.dict(
  185. pip_state.__salt__,
  186. {"cmd.run_all": mock, "pip.list": pip_list, "pip.install": pip_install},
  187. ):
  188. ret = pip_state.installed(
  189. "https://pypi.python.org/packages/source/S/SaltTesting/"
  190. "SaltTesting-0.5.0.tar.gz"
  191. "#md5=e6760af92b7165f8be53b5763e40bc24"
  192. )
  193. self.assertSaltTrueReturn({"test": ret})
  194. self.assertInSaltComment(
  195. "All packages were successfully installed", {"test": ret}
  196. )
  197. self.assertInSaltReturn(
  198. "Installed",
  199. {"test": ret},
  200. (
  201. "changes",
  202. "https://pypi.python.org/packages/source/S/"
  203. "SaltTesting/SaltTesting-0.5.0.tar.gz"
  204. "#md5=e6760af92b7165f8be53b5763e40bc24==???",
  205. ),
  206. )
  207. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  208. pip_list = MagicMock(return_value={"SaltTesting": "0.5.0"})
  209. pip_install = MagicMock(
  210. return_value={"retcode": 0, "stderr": "", "stdout": "Cloned!"}
  211. )
  212. with patch.dict(
  213. pip_state.__salt__,
  214. {"cmd.run_all": mock, "pip.list": pip_list, "pip.install": pip_install},
  215. ):
  216. with patch.dict(pip_state.__opts__, {"test": False}):
  217. ret = pip_state.installed(
  218. "git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting"
  219. )
  220. self.assertSaltTrueReturn({"test": ret})
  221. self.assertInSaltComment(
  222. "packages are already installed", {"test": ret}
  223. )
  224. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  225. pip_list = MagicMock(return_value={"pep8": "1.3.1"})
  226. pip_install = MagicMock(return_value={"retcode": 0})
  227. with patch.dict(
  228. pip_state.__salt__,
  229. {"cmd.run_all": mock, "pip.list": pip_list, "pip.install": pip_install},
  230. ):
  231. with patch.dict(pip_state.__opts__, {"test": False}):
  232. ret = pip_state.installed(
  233. "arbitrary ID that should be ignored due to requirements specified",
  234. requirements="/tmp/non-existing-requirements.txt",
  235. )
  236. self.assertSaltTrueReturn({"test": ret})
  237. # Test VCS installations using git+git://
  238. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  239. pip_list = MagicMock(return_value={"SaltTesting": "0.5.0"})
  240. pip_install = MagicMock(
  241. return_value={"retcode": 0, "stderr": "", "stdout": "Cloned!"}
  242. )
  243. with patch.dict(
  244. pip_state.__salt__,
  245. {"cmd.run_all": mock, "pip.list": pip_list, "pip.install": pip_install},
  246. ):
  247. with patch.dict(pip_state.__opts__, {"test": False}):
  248. ret = pip_state.installed(
  249. "git+git://github.com/saltstack/salt-testing.git#egg=SaltTesting"
  250. )
  251. self.assertSaltTrueReturn({"test": ret})
  252. self.assertInSaltComment(
  253. "packages are already installed", {"test": ret}
  254. )
  255. def test_install_requirements_custom_pypi(self):
  256. """
  257. test requirement parsing for both when a custom
  258. pypi index-url is set and when it is not and
  259. the requirement is already installed.
  260. """
  261. # create requirements file
  262. req_filename = os.path.join(
  263. RUNTIME_VARS.TMP_STATE_TREE, "custom-pypi-requirements.txt"
  264. )
  265. with salt.utils.files.fopen(req_filename, "wb") as reqf:
  266. reqf.write(b"pep8\n")
  267. site_pkgs = "/tmp/pip-env/lib/python3.7/site-packages"
  268. check_stdout = [
  269. (
  270. "Looking in indexes: https://custom-pypi-url.org,"
  271. "https://pypi.org/simple/\nRequirement already satisfied: pep8 in {1}"
  272. "(from -r /tmp/files/prod/{0} (line 1)) (1.7.1)".format(
  273. req_filename, site_pkgs
  274. )
  275. ),
  276. (
  277. "Requirement already satisfied: pep8 in {1}"
  278. "(from -r /tmp/files/prod/{0} (line1)) (1.7.1)".format(
  279. req_filename, site_pkgs
  280. )
  281. ),
  282. ]
  283. pip_version = pip.__version__
  284. mock_pip_version = MagicMock(return_value=pip_version)
  285. for stdout in check_stdout:
  286. pip_install = MagicMock(return_value={"retcode": 0, "stdout": stdout})
  287. with patch.dict(pip_state.__salt__, {"pip.version": mock_pip_version}):
  288. with patch.dict(pip_state.__salt__, {"pip.install": pip_install}):
  289. ret = pip_state.installed(name="", requirements=req_filename)
  290. self.assertSaltTrueReturn({"test": ret})
  291. assert "Requirements were already installed." == ret["comment"]
  292. def test_install_requirements_custom_pypi_changes(self):
  293. """
  294. test requirement parsing for both when a custom
  295. pypi index-url is set and when it is not and
  296. the requirement is not installed.
  297. """
  298. # create requirements file
  299. req_filename = os.path.join(
  300. RUNTIME_VARS.TMP_STATE_TREE, "custom-pypi-requirements.txt"
  301. )
  302. with salt.utils.files.fopen(req_filename, "wb") as reqf:
  303. reqf.write(b"pep8\n")
  304. site_pkgs = "/tmp/pip-env/lib/python3.7/site-packages"
  305. check_stdout = [
  306. (
  307. "Looking in indexes: https://custom-pypi-url.org,"
  308. "https://pypi.org/simple/\nCollecting pep8\n Using cached"
  309. "https://custom-pypi-url.org//packages/42/3f/669429cef5acb4/pep8-1.7.1-py2.py3-none-any.whl"
  310. " (41 kB)\nInstalling collected packages: pep8\nSuccessfully installed pep8-1.7.1"
  311. ),
  312. (
  313. "Collecting pep8\n Using cached"
  314. "https://custom-pypi-url.org//packages/42/3f/669429cef5acb4/pep8-1.7.1-py2.py3-none-any.whl"
  315. " (41 kB)\nInstalling collected packages: pep8\nSuccessfully installed pep8-1.7.1"
  316. ),
  317. ]
  318. pip_version = pip.__version__
  319. mock_pip_version = MagicMock(return_value=pip_version)
  320. for stdout in check_stdout:
  321. pip_install = MagicMock(return_value={"retcode": 0, "stdout": stdout})
  322. with patch.dict(pip_state.__salt__, {"pip.version": mock_pip_version}):
  323. with patch.dict(pip_state.__salt__, {"pip.install": pip_install}):
  324. ret = pip_state.installed(name="", requirements=req_filename)
  325. self.assertSaltTrueReturn({"test": ret})
  326. assert (
  327. "Successfully processed requirements file {0}.".format(
  328. req_filename
  329. )
  330. == ret["comment"]
  331. )
  332. def test_install_in_editable_mode(self):
  333. """
  334. Check that `name` parameter containing bad characters is not parsed by
  335. pip when package is being installed in editable mode.
  336. For more information, see issue #21890.
  337. """
  338. mock = MagicMock(return_value={"retcode": 0, "stdout": ""})
  339. pip_list = MagicMock(return_value={})
  340. pip_install = MagicMock(
  341. return_value={"retcode": 0, "stderr": "", "stdout": "Cloned!"}
  342. )
  343. pip_version = MagicMock(return_value="10.0.1")
  344. with patch.dict(
  345. pip_state.__salt__,
  346. {
  347. "cmd.run_all": mock,
  348. "pip.list": pip_list,
  349. "pip.install": pip_install,
  350. "pip.version": pip_version,
  351. },
  352. ):
  353. ret = pip_state.installed(
  354. "state@name", cwd="/path/to/project", editable=["."]
  355. )
  356. self.assertSaltTrueReturn({"test": ret})
  357. self.assertInSaltComment("successfully installed", {"test": ret})
  358. class PipStateUtilsTest(TestCase):
  359. def test_has_internal_exceptions_mod_function(self):
  360. assert pip_state.pip_has_internal_exceptions_mod("10.0")
  361. assert pip_state.pip_has_internal_exceptions_mod("18.1")
  362. assert not pip_state.pip_has_internal_exceptions_mod("9.99")
  363. def test_has_exceptions_mod_function(self):
  364. assert pip_state.pip_has_exceptions_mod("1.0")
  365. assert not pip_state.pip_has_exceptions_mod("0.1")
  366. assert not pip_state.pip_has_exceptions_mod("10.0")
  367. def test_pip_purge_method_with_pip(self):
  368. mock_modules = sys.modules.copy()
  369. mock_modules.pop("pip", None)
  370. mock_modules["pip"] = object()
  371. with patch("sys.modules", mock_modules):
  372. pip_state.purge_pip()
  373. assert "pip" not in mock_modules
  374. def test_pip_purge_method_without_pip(self):
  375. mock_modules = sys.modules.copy()
  376. mock_modules.pop("pip", None)
  377. with patch("sys.modules", mock_modules):
  378. pip_state.purge_pip()
  379. @skipIf(
  380. salt.utils.path.which_bin(KNOWN_BINARY_NAMES) is None, "virtualenv not installed"
  381. )
  382. class PipStateInstallationErrorTest(TestCase):
  383. @pytest.mark.slow_test(seconds=120) # Test takes >60 and <=120 seconds
  384. def test_importable_installation_error(self):
  385. extra_requirements = []
  386. for name, version in salt.version.dependency_information():
  387. if name in ["PyYAML"]:
  388. extra_requirements.append("{}=={}".format(name, version))
  389. failures = {}
  390. pip_version_requirements = [
  391. # Latest pip 8
  392. "<9.0",
  393. # Latest pip 9
  394. "<10.0",
  395. # Latest pip 18
  396. "<19.0",
  397. # Latest pip 19
  398. "<20.0",
  399. # Latest pip 20
  400. "<21.0",
  401. # Latest pip
  402. None,
  403. ]
  404. code = dedent(
  405. """\
  406. import sys
  407. import traceback
  408. try:
  409. import salt.states.pip_state
  410. salt.states.pip_state.InstallationError
  411. except ImportError as exc:
  412. traceback.print_exc(exc, file=sys.stdout)
  413. sys.stdout.flush()
  414. sys.exit(1)
  415. except AttributeError as exc:
  416. traceback.print_exc(exc, file=sys.stdout)
  417. sys.stdout.flush()
  418. sys.exit(2)
  419. except Exception as exc:
  420. traceback.print_exc(exc, file=sys.stdout)
  421. sys.stdout.flush()
  422. sys.exit(3)
  423. sys.exit(0)
  424. """
  425. )
  426. for requirement in list(pip_version_requirements):
  427. try:
  428. with VirtualEnv() as venv:
  429. venv.install(*extra_requirements)
  430. if requirement:
  431. venv.install("pip{}".format(requirement))
  432. try:
  433. subprocess.check_output([venv.venv_python, "-c", code])
  434. except subprocess.CalledProcessError as exc:
  435. if exc.returncode == 1:
  436. failures[requirement] = "Failed to import pip:\n{}".format(
  437. exc.output
  438. )
  439. elif exc.returncode == 2:
  440. failures[
  441. requirement
  442. ] = "Failed to import InstallationError from pip:\n{}".format(
  443. exc.output
  444. )
  445. else:
  446. failures[requirement] = exc.output
  447. except Exception as exc: # pylint: disable=broad-except
  448. failures[requirement] = str(exc)
  449. if failures:
  450. errors = ""
  451. for requirement, exception in failures.items():
  452. errors += "pip{}: {}\n\n".format(requirement or "", exception)
  453. self.fail(
  454. "Failed to get InstallationError exception under at least one pip version:\n{}".format(
  455. errors
  456. )
  457. )