test_pip.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. # -*- coding: utf-8 -*-
  2. """
  3. tests.integration.modules.pip
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  5. """
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import os
  8. import pprint
  9. import re
  10. import shutil
  11. import sys
  12. import tempfile
  13. import pytest
  14. import salt.utils.files
  15. import salt.utils.path
  16. import salt.utils.platform
  17. from salt.modules.virtualenv_mod import KNOWN_BINARY_NAMES
  18. from tests.support.case import ModuleCase
  19. from tests.support.helpers import patched_environ, slowTest
  20. from tests.support.runtests import RUNTIME_VARS
  21. from tests.support.unit import skipIf
  22. @skipIf(
  23. salt.utils.path.which_bin(KNOWN_BINARY_NAMES) is None, "virtualenv not installed"
  24. )
  25. @pytest.mark.windows_whitelisted
  26. class PipModuleTest(ModuleCase):
  27. def setUp(self):
  28. super(PipModuleTest, self).setUp()
  29. self.venv_test_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  30. # Remove the venv test directory
  31. self.addCleanup(shutil.rmtree, self.venv_test_dir, ignore_errors=True)
  32. self.venv_dir = os.path.join(self.venv_test_dir, "venv")
  33. self.pip_temp = os.path.join(self.venv_test_dir, ".pip-temp")
  34. if not os.path.isdir(self.pip_temp):
  35. os.makedirs(self.pip_temp)
  36. self.patched_environ = patched_environ(
  37. PIP_SOURCE_DIR="",
  38. PIP_BUILD_DIR="",
  39. __cleanup__=[k for k in os.environ if k.startswith("PIP_")],
  40. )
  41. self.patched_environ.__enter__()
  42. self.addCleanup(self.patched_environ.__exit__)
  43. def _create_virtualenv(self, path):
  44. """
  45. The reason why the virtualenv creation is proxied by this function is mostly
  46. because under windows, we can't seem to properly create a virtualenv off of
  47. another virtualenv(we can on linux) and also because, we really don't want to
  48. test virtualenv creation off of another virtualenv, we want a virtualenv created
  49. from the original python.
  50. Also, one windows, we must also point to the virtualenv binary outside the existing
  51. virtualenv because it will fail otherwise
  52. """
  53. try:
  54. if salt.utils.platform.is_windows():
  55. python = os.path.join(sys.real_prefix, os.path.basename(sys.executable))
  56. else:
  57. python_binary_names = [
  58. "python{}.{}".format(*sys.version_info),
  59. "python{}".format(*sys.version_info),
  60. "python",
  61. ]
  62. for binary_name in python_binary_names:
  63. python = os.path.join(sys.real_prefix, "bin", binary_name)
  64. if os.path.exists(python):
  65. break
  66. else:
  67. self.fail(
  68. "Couldn't find a python binary name under '{}' matching: {}".format(
  69. os.path.join(sys.real_prefix, "bin"), python_binary_names
  70. )
  71. )
  72. # We're running off a virtualenv, and we don't want to create a virtualenv off of
  73. # a virtualenv
  74. kwargs = {"python": python}
  75. except AttributeError:
  76. # We're running off of the system python
  77. kwargs = {}
  78. self.run_function("virtualenv.create", [path], **kwargs)
  79. def _check_download_error(self, ret):
  80. """
  81. Checks to see if a download error looks transitory
  82. """
  83. return any(w in ret for w in ["URLError", "Download error"])
  84. def pip_successful_install(self, target, expect=("irc3-plugins-test", "pep8",)):
  85. """
  86. isolate regex for extracting `successful install` message from pip
  87. """
  88. expect = set(expect)
  89. expect_str = "|".join(expect)
  90. success = re.search(
  91. r"^.*Successfully installed\s([^\n]+)(?:Clean.*)?", target, re.M | re.S
  92. )
  93. success_for = (
  94. re.findall(
  95. r"({0})(?:-(?:[\d\.-]))?".format(expect_str), success.groups()[0]
  96. )
  97. if success
  98. else []
  99. )
  100. return expect.issubset(set(success_for))
  101. @slowTest
  102. def test_issue_2087_missing_pip(self):
  103. # Let's create the testing virtualenv
  104. self._create_virtualenv(self.venv_dir)
  105. # Let's remove the pip binary
  106. pip_bin = os.path.join(self.venv_dir, "bin", "pip")
  107. site_dir = self.run_function(
  108. "virtualenv.get_distribution_path", [self.venv_dir, "pip"]
  109. )
  110. if salt.utils.platform.is_windows():
  111. pip_bin = os.path.join(self.venv_dir, "Scripts", "pip.exe")
  112. site_dir = os.path.join(self.venv_dir, "lib", "site-packages")
  113. if not os.path.isfile(pip_bin):
  114. self.skipTest("Failed to find the pip binary to the test virtualenv")
  115. os.remove(pip_bin)
  116. # Also remove the pip dir from site-packages
  117. # This is needed now that we're using python -m pip instead of the
  118. # pip binary directly. python -m pip will still work even if the
  119. # pip binary is missing
  120. shutil.rmtree(os.path.join(site_dir, "pip"))
  121. # Let's run a pip depending functions
  122. for func in ("pip.freeze", "pip.list"):
  123. ret = self.run_function(func, bin_env=self.venv_dir)
  124. self.assertIn(
  125. "Command required for '{0}' not found: "
  126. "Could not find a `pip` binary".format(func),
  127. ret,
  128. )
  129. @slowTest
  130. def test_requirements_as_list_of_chains__cwd_set__absolute_file_path(self):
  131. self._create_virtualenv(self.venv_dir)
  132. # Create a requirements file that depends on another one.
  133. req1_filename = os.path.join(self.venv_dir, "requirements1.txt")
  134. req1b_filename = os.path.join(self.venv_dir, "requirements1b.txt")
  135. req2_filename = os.path.join(self.venv_dir, "requirements2.txt")
  136. req2b_filename = os.path.join(self.venv_dir, "requirements2b.txt")
  137. with salt.utils.files.fopen(req1_filename, "w") as f:
  138. f.write("-r requirements1b.txt\n")
  139. with salt.utils.files.fopen(req1b_filename, "w") as f:
  140. f.write("irc3-plugins-test\n")
  141. with salt.utils.files.fopen(req2_filename, "w") as f:
  142. f.write("-r requirements2b.txt\n")
  143. with salt.utils.files.fopen(req2b_filename, "w") as f:
  144. f.write("pep8\n")
  145. requirements_list = [req1_filename, req2_filename]
  146. ret = self.run_function(
  147. "pip.install",
  148. requirements=requirements_list,
  149. bin_env=self.venv_dir,
  150. cwd=self.venv_dir,
  151. )
  152. if not isinstance(ret, dict):
  153. self.fail(
  154. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  155. ret
  156. )
  157. )
  158. try:
  159. self.assertEqual(ret["retcode"], 0)
  160. found = self.pip_successful_install(ret["stdout"])
  161. self.assertTrue(found)
  162. except KeyError as exc:
  163. self.fail(
  164. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  165. exc, pprint.pformat(ret)
  166. )
  167. )
  168. @slowTest
  169. def test_requirements_as_list_of_chains__cwd_not_set__absolute_file_path(self):
  170. self._create_virtualenv(self.venv_dir)
  171. # Create a requirements file that depends on another one.
  172. req1_filename = os.path.join(self.venv_dir, "requirements1.txt")
  173. req1b_filename = os.path.join(self.venv_dir, "requirements1b.txt")
  174. req2_filename = os.path.join(self.venv_dir, "requirements2.txt")
  175. req2b_filename = os.path.join(self.venv_dir, "requirements2b.txt")
  176. with salt.utils.files.fopen(req1_filename, "w") as f:
  177. f.write("-r requirements1b.txt\n")
  178. with salt.utils.files.fopen(req1b_filename, "w") as f:
  179. f.write("irc3-plugins-test\n")
  180. with salt.utils.files.fopen(req2_filename, "w") as f:
  181. f.write("-r requirements2b.txt\n")
  182. with salt.utils.files.fopen(req2b_filename, "w") as f:
  183. f.write("pep8\n")
  184. requirements_list = [req1_filename, req2_filename]
  185. ret = self.run_function(
  186. "pip.install", requirements=requirements_list, bin_env=self.venv_dir
  187. )
  188. if not isinstance(ret, dict):
  189. self.fail(
  190. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  191. ret
  192. )
  193. )
  194. try:
  195. self.assertEqual(ret["retcode"], 0)
  196. found = self.pip_successful_install(ret["stdout"])
  197. self.assertTrue(found)
  198. except KeyError as exc:
  199. self.fail(
  200. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  201. exc, pprint.pformat(ret)
  202. )
  203. )
  204. @slowTest
  205. def test_requirements_as_list__absolute_file_path(self):
  206. self._create_virtualenv(self.venv_dir)
  207. req1_filename = os.path.join(self.venv_dir, "requirements.txt")
  208. req2_filename = os.path.join(self.venv_dir, "requirements2.txt")
  209. with salt.utils.files.fopen(req1_filename, "w") as f:
  210. f.write("irc3-plugins-test\n")
  211. with salt.utils.files.fopen(req2_filename, "w") as f:
  212. f.write("pep8\n")
  213. requirements_list = [req1_filename, req2_filename]
  214. ret = self.run_function(
  215. "pip.install", requirements=requirements_list, bin_env=self.venv_dir
  216. )
  217. if not isinstance(ret, dict):
  218. self.fail(
  219. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  220. ret
  221. )
  222. )
  223. try:
  224. self.assertEqual(ret["retcode"], 0)
  225. found = self.pip_successful_install(ret["stdout"])
  226. self.assertTrue(found)
  227. except KeyError as exc:
  228. self.fail(
  229. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  230. exc, pprint.pformat(ret)
  231. )
  232. )
  233. @slowTest
  234. def test_requirements_as_list__non_absolute_file_path(self):
  235. self._create_virtualenv(self.venv_dir)
  236. # Create a requirements file that depends on another one.
  237. req1_filename = "requirements.txt"
  238. req2_filename = "requirements2.txt"
  239. req_cwd = self.venv_dir
  240. req1_filepath = os.path.join(req_cwd, req1_filename)
  241. req2_filepath = os.path.join(req_cwd, req2_filename)
  242. with salt.utils.files.fopen(req1_filepath, "w") as f:
  243. f.write("irc3-plugins-test\n")
  244. with salt.utils.files.fopen(req2_filepath, "w") as f:
  245. f.write("pep8\n")
  246. requirements_list = [req1_filename, req2_filename]
  247. ret = self.run_function(
  248. "pip.install",
  249. requirements=requirements_list,
  250. bin_env=self.venv_dir,
  251. cwd=req_cwd,
  252. )
  253. if not isinstance(ret, dict):
  254. self.fail(
  255. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  256. ret
  257. )
  258. )
  259. try:
  260. self.assertEqual(ret["retcode"], 0)
  261. found = self.pip_successful_install(ret["stdout"])
  262. self.assertTrue(found)
  263. except KeyError as exc:
  264. self.fail(
  265. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  266. exc, pprint.pformat(ret)
  267. )
  268. )
  269. @slowTest
  270. def test_chained_requirements__absolute_file_path(self):
  271. self._create_virtualenv(self.venv_dir)
  272. # Create a requirements file that depends on another one.
  273. req1_filename = os.path.join(self.venv_dir, "requirements.txt")
  274. req2_filename = os.path.join(self.venv_dir, "requirements2.txt")
  275. with salt.utils.files.fopen(req1_filename, "w") as f:
  276. f.write("-r requirements2.txt")
  277. with salt.utils.files.fopen(req2_filename, "w") as f:
  278. f.write("pep8")
  279. ret = self.run_function(
  280. "pip.install", requirements=req1_filename, bin_env=self.venv_dir
  281. )
  282. if not isinstance(ret, dict):
  283. self.fail(
  284. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  285. ret
  286. )
  287. )
  288. try:
  289. self.assertEqual(ret["retcode"], 0)
  290. self.assertIn("installed pep8", ret["stdout"])
  291. except KeyError as exc:
  292. self.fail(
  293. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  294. exc, pprint.pformat(ret)
  295. )
  296. )
  297. @slowTest
  298. def test_chained_requirements__non_absolute_file_path(self):
  299. self._create_virtualenv(self.venv_dir)
  300. # Create a requirements file that depends on another one.
  301. req_basepath = self.venv_dir
  302. req1_filename = "requirements.txt"
  303. req2_filename = "requirements2.txt"
  304. req1_file = os.path.join(self.venv_dir, req1_filename)
  305. req2_file = os.path.join(self.venv_dir, req2_filename)
  306. with salt.utils.files.fopen(req1_file, "w") as f:
  307. f.write("-r requirements2.txt")
  308. with salt.utils.files.fopen(req2_file, "w") as f:
  309. f.write("pep8")
  310. ret = self.run_function(
  311. "pip.install",
  312. requirements=req1_filename,
  313. cwd=req_basepath,
  314. bin_env=self.venv_dir,
  315. )
  316. if not isinstance(ret, dict):
  317. self.fail(
  318. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  319. ret
  320. )
  321. )
  322. try:
  323. self.assertEqual(ret["retcode"], 0)
  324. self.assertIn("installed pep8", ret["stdout"])
  325. except KeyError as exc:
  326. self.fail(
  327. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  328. exc, pprint.pformat(ret)
  329. )
  330. )
  331. @slowTest
  332. def test_issue_4805_nested_requirements(self):
  333. self._create_virtualenv(self.venv_dir)
  334. # Create a requirements file that depends on another one.
  335. req1_filename = os.path.join(self.venv_dir, "requirements.txt")
  336. req2_filename = os.path.join(self.venv_dir, "requirements2.txt")
  337. with salt.utils.files.fopen(req1_filename, "w") as f:
  338. f.write("-r requirements2.txt")
  339. with salt.utils.files.fopen(req2_filename, "w") as f:
  340. f.write("pep8")
  341. ret = self.run_function(
  342. "pip.install",
  343. requirements=req1_filename,
  344. bin_env=self.venv_dir,
  345. timeout=300,
  346. )
  347. if not isinstance(ret, dict):
  348. self.fail(
  349. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  350. ret
  351. )
  352. )
  353. try:
  354. if self._check_download_error(ret["stdout"]):
  355. self.skipTest("Test skipped due to pip download error")
  356. self.assertEqual(ret["retcode"], 0)
  357. self.assertIn("installed pep8", ret["stdout"])
  358. except KeyError as exc:
  359. self.fail(
  360. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  361. exc, pprint.pformat(ret)
  362. )
  363. )
  364. @slowTest
  365. def test_pip_uninstall(self):
  366. # Let's create the testing virtualenv
  367. self._create_virtualenv(self.venv_dir)
  368. ret = self.run_function("pip.install", ["pep8"], bin_env=self.venv_dir)
  369. if not isinstance(ret, dict):
  370. self.fail(
  371. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  372. ret
  373. )
  374. )
  375. try:
  376. if self._check_download_error(ret["stdout"]):
  377. self.skipTest("Test skipped due to pip download error")
  378. self.assertEqual(ret["retcode"], 0)
  379. self.assertIn("installed pep8", ret["stdout"])
  380. except KeyError as exc:
  381. self.fail(
  382. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  383. exc, pprint.pformat(ret)
  384. )
  385. )
  386. ret = self.run_function("pip.uninstall", ["pep8"], bin_env=self.venv_dir)
  387. if not isinstance(ret, dict):
  388. self.fail(
  389. "The 'pip.uninstall' command did not return the excepted dictionary. Output:\n{}".format(
  390. ret
  391. )
  392. )
  393. try:
  394. self.assertEqual(ret["retcode"], 0)
  395. self.assertIn("uninstalled pep8", ret["stdout"])
  396. except KeyError as exc:
  397. self.fail(
  398. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  399. exc, pprint.pformat(ret)
  400. )
  401. )
  402. @slowTest
  403. def test_pip_install_upgrade(self):
  404. # Create the testing virtualenv
  405. self._create_virtualenv(self.venv_dir)
  406. ret = self.run_function("pip.install", ["pep8==1.3.4"], bin_env=self.venv_dir)
  407. if not isinstance(ret, dict):
  408. self.fail(
  409. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  410. ret
  411. )
  412. )
  413. try:
  414. if self._check_download_error(ret["stdout"]):
  415. self.skipTest("Test skipped due to pip download error")
  416. self.assertEqual(ret["retcode"], 0)
  417. self.assertIn("installed pep8", ret["stdout"])
  418. except KeyError as exc:
  419. self.fail(
  420. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  421. exc, pprint.pformat(ret)
  422. )
  423. )
  424. ret = self.run_function(
  425. "pip.install", ["pep8"], bin_env=self.venv_dir, upgrade=True
  426. )
  427. if not isinstance(ret, dict):
  428. self.fail(
  429. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  430. ret
  431. )
  432. )
  433. try:
  434. if self._check_download_error(ret["stdout"]):
  435. self.skipTest("Test skipped due to pip download error")
  436. self.assertEqual(ret["retcode"], 0)
  437. self.assertIn("installed pep8", ret["stdout"])
  438. except KeyError as exc:
  439. self.fail(
  440. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  441. exc, pprint.pformat(ret)
  442. )
  443. )
  444. ret = self.run_function("pip.uninstall", ["pep8"], bin_env=self.venv_dir)
  445. if not isinstance(ret, dict):
  446. self.fail(
  447. "The 'pip.uninstall' command did not return the excepted dictionary. Output:\n{}".format(
  448. ret
  449. )
  450. )
  451. try:
  452. self.assertEqual(ret["retcode"], 0)
  453. self.assertIn("uninstalled pep8", ret["stdout"])
  454. except KeyError as exc:
  455. self.fail(
  456. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  457. exc, pprint.pformat(ret)
  458. )
  459. )
  460. @slowTest
  461. def test_pip_install_multiple_editables(self):
  462. editables = [
  463. "git+https://github.com/jek/blinker.git#egg=Blinker",
  464. "git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting",
  465. ]
  466. # Create the testing virtualenv
  467. self._create_virtualenv(self.venv_dir)
  468. ret = self.run_function(
  469. "pip.install",
  470. [],
  471. editable="{0}".format(",".join(editables)),
  472. bin_env=self.venv_dir,
  473. )
  474. if not isinstance(ret, dict):
  475. self.fail(
  476. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  477. ret
  478. )
  479. )
  480. try:
  481. if self._check_download_error(ret["stdout"]):
  482. self.skipTest("Test skipped due to pip download error")
  483. self.assertEqual(ret["retcode"], 0)
  484. self.assertIn("Successfully installed Blinker SaltTesting", ret["stdout"])
  485. except KeyError as exc:
  486. self.fail(
  487. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  488. exc, pprint.pformat(ret)
  489. )
  490. )
  491. @slowTest
  492. def test_pip_install_multiple_editables_and_pkgs(self):
  493. editables = [
  494. "git+https://github.com/jek/blinker.git#egg=Blinker",
  495. "git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting",
  496. ]
  497. # Create the testing virtualenv
  498. self._create_virtualenv(self.venv_dir)
  499. ret = self.run_function(
  500. "pip.install",
  501. ["pep8"],
  502. editable="{0}".format(",".join(editables)),
  503. bin_env=self.venv_dir,
  504. )
  505. if not isinstance(ret, dict):
  506. self.fail(
  507. "The 'pip.install' command did not return the excepted dictionary. Output:\n{}".format(
  508. ret
  509. )
  510. )
  511. try:
  512. if self._check_download_error(ret["stdout"]):
  513. self.skipTest("Test skipped due to pip download error")
  514. self.assertEqual(ret["retcode"], 0)
  515. for package in ("Blinker", "SaltTesting", "pep8"):
  516. self.assertRegex(
  517. ret["stdout"],
  518. r"(?:.*)(Successfully installed)(?:.*)({0})(?:.*)".format(package),
  519. )
  520. except KeyError as exc:
  521. self.fail(
  522. "The returned dictionary is missing an expected key. Error: '{}'. Dictionary: {}".format(
  523. exc, pprint.pformat(ret)
  524. )
  525. )
  526. @skipIf(not os.path.isfile("pip3"), "test where pip3 is installed")
  527. @skipIf(
  528. salt.utils.platform.is_windows(), "test specific for linux usage of /bin/python"
  529. )
  530. def test_system_pip3(self):
  531. self.run_function(
  532. "pip.install", pkgs=["lazyimport==0.0.1"], bin_env="/bin/pip3"
  533. )
  534. ret1 = self.run_function("cmd.run", "/bin/pip3 freeze | grep lazyimport")
  535. self.run_function("pip.uninstall", pkgs=["lazyimport"], bin_env="/bin/pip3")
  536. ret2 = self.run_function("cmd.run", "/bin/pip3 freeze | grep lazyimport")
  537. assert "lazyimport==0.0.1" in ret1
  538. assert ret2 == ""