test_cp.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import, print_function, unicode_literals
  3. import hashlib
  4. import logging
  5. import os
  6. import shutil
  7. import signal
  8. import tempfile
  9. import textwrap
  10. import time
  11. import uuid
  12. import psutil
  13. import pytest
  14. import salt.ext.six as six
  15. import salt.utils.files
  16. import salt.utils.path
  17. import salt.utils.platform
  18. import salt.utils.stringutils
  19. from saltfactories.utils.ports import get_unused_localhost_port
  20. from tests.support.case import ModuleCase
  21. from tests.support.helpers import skip_if_not_root, slowTest, with_tempfile
  22. from tests.support.runtests import RUNTIME_VARS
  23. from tests.support.unit import skipIf
  24. log = logging.getLogger(__name__)
  25. @pytest.mark.windows_whitelisted
  26. class CPModuleTest(ModuleCase):
  27. """
  28. Validate the cp module
  29. """
  30. def run_function(self, *args, **kwargs): # pylint: disable=arguments-differ
  31. """
  32. Ensure that results are decoded
  33. TODO: maybe move this behavior to ModuleCase itself?
  34. """
  35. return salt.utils.data.decode(
  36. super(CPModuleTest, self).run_function(*args, **kwargs)
  37. )
  38. @with_tempfile()
  39. @slowTest
  40. def test_get_file(self, tgt):
  41. """
  42. cp.get_file
  43. """
  44. self.run_function("cp.get_file", ["salt://grail/scene33", tgt])
  45. with salt.utils.files.fopen(tgt, "r") as scene:
  46. data = salt.utils.stringutils.to_unicode(scene.read())
  47. self.assertIn("KNIGHT: They're nervous, sire.", data)
  48. self.assertNotIn("bacon", data)
  49. @slowTest
  50. def test_get_file_to_dir(self):
  51. """
  52. cp.get_file
  53. """
  54. tgt = os.path.join(RUNTIME_VARS.TMP, "")
  55. self.run_function("cp.get_file", ["salt://grail/scene33", tgt])
  56. with salt.utils.files.fopen(tgt + "scene33", "r") as scene:
  57. data = salt.utils.stringutils.to_unicode(scene.read())
  58. self.assertIn("KNIGHT: They're nervous, sire.", data)
  59. self.assertNotIn("bacon", data)
  60. @with_tempfile()
  61. @skipIf(
  62. salt.utils.platform.is_windows() and six.PY3,
  63. "This test hangs on Windows on Py3",
  64. )
  65. def test_get_file_templated_paths(self, tgt):
  66. """
  67. cp.get_file
  68. """
  69. self.run_function(
  70. "cp.get_file",
  71. [
  72. "salt://{{grains.test_grain}}",
  73. tgt.replace("cheese", "{{grains.test_grain}}"),
  74. ],
  75. template="jinja",
  76. )
  77. with salt.utils.files.fopen(tgt, "r") as cheese:
  78. data = salt.utils.stringutils.to_unicode(cheese.read())
  79. self.assertIn("Gromit", data)
  80. self.assertNotIn("bacon", data)
  81. @with_tempfile()
  82. @slowTest
  83. def test_get_file_gzipped(self, tgt):
  84. """
  85. cp.get_file
  86. """
  87. src = os.path.join(RUNTIME_VARS.FILES, "file", "base", "file.big")
  88. with salt.utils.files.fopen(src, "rb") as fp_:
  89. hash_str = hashlib.md5(fp_.read()).hexdigest()
  90. self.run_function("cp.get_file", ["salt://file.big", tgt], gzip=5)
  91. with salt.utils.files.fopen(tgt, "rb") as scene:
  92. data = scene.read()
  93. self.assertEqual(hash_str, hashlib.md5(data).hexdigest())
  94. data = salt.utils.stringutils.to_unicode(data)
  95. self.assertIn("KNIGHT: They're nervous, sire.", data)
  96. self.assertNotIn("bacon", data)
  97. @slowTest
  98. def test_get_file_makedirs(self):
  99. """
  100. cp.get_file
  101. """
  102. tgt = os.path.join(RUNTIME_VARS.TMP, "make", "dirs", "scene33")
  103. self.run_function("cp.get_file", ["salt://grail/scene33", tgt], makedirs=True)
  104. self.addCleanup(
  105. shutil.rmtree, os.path.join(RUNTIME_VARS.TMP, "make"), ignore_errors=True
  106. )
  107. with salt.utils.files.fopen(tgt, "r") as scene:
  108. data = salt.utils.stringutils.to_unicode(scene.read())
  109. self.assertIn("KNIGHT: They're nervous, sire.", data)
  110. self.assertNotIn("bacon", data)
  111. @with_tempfile()
  112. @slowTest
  113. def test_get_template(self, tgt):
  114. """
  115. cp.get_template
  116. """
  117. self.run_function(
  118. "cp.get_template", ["salt://grail/scene33", tgt], spam="bacon"
  119. )
  120. with salt.utils.files.fopen(tgt, "r") as scene:
  121. data = salt.utils.stringutils.to_unicode(scene.read())
  122. self.assertIn("bacon", data)
  123. self.assertNotIn("spam", data)
  124. @slowTest
  125. def test_get_dir(self):
  126. """
  127. cp.get_dir
  128. """
  129. tgt = os.path.join(RUNTIME_VARS.TMP, "many")
  130. self.run_function("cp.get_dir", ["salt://grail", tgt])
  131. self.assertIn("grail", os.listdir(tgt))
  132. self.assertIn("36", os.listdir(os.path.join(tgt, "grail")))
  133. self.assertIn("empty", os.listdir(os.path.join(tgt, "grail")))
  134. self.assertIn("scene", os.listdir(os.path.join(tgt, "grail", "36")))
  135. @slowTest
  136. def test_get_dir_templated_paths(self):
  137. """
  138. cp.get_dir
  139. """
  140. tgt = os.path.join(RUNTIME_VARS.TMP, "many")
  141. self.run_function(
  142. "cp.get_dir",
  143. ["salt://{{grains.script}}", tgt.replace("many", "{{grains.alot}}")],
  144. )
  145. self.assertIn("grail", os.listdir(tgt))
  146. self.assertIn("36", os.listdir(os.path.join(tgt, "grail")))
  147. self.assertIn("empty", os.listdir(os.path.join(tgt, "grail")))
  148. self.assertIn("scene", os.listdir(os.path.join(tgt, "grail", "36")))
  149. # cp.get_url tests
  150. @with_tempfile()
  151. @slowTest
  152. def test_get_url(self, tgt):
  153. """
  154. cp.get_url with salt:// source given
  155. """
  156. self.run_function("cp.get_url", ["salt://grail/scene33", tgt])
  157. with salt.utils.files.fopen(tgt, "r") as scene:
  158. data = salt.utils.stringutils.to_unicode(scene.read())
  159. self.assertIn("KNIGHT: They're nervous, sire.", data)
  160. self.assertNotIn("bacon", data)
  161. @slowTest
  162. def test_get_url_makedirs(self):
  163. """
  164. cp.get_url
  165. """
  166. tgt = os.path.join(RUNTIME_VARS.TMP, "make", "dirs", "scene33")
  167. self.run_function("cp.get_url", ["salt://grail/scene33", tgt], makedirs=True)
  168. self.addCleanup(
  169. shutil.rmtree, os.path.join(RUNTIME_VARS.TMP, "make"), ignore_errors=True
  170. )
  171. with salt.utils.files.fopen(tgt, "r") as scene:
  172. data = salt.utils.stringutils.to_unicode(scene.read())
  173. self.assertIn("KNIGHT: They're nervous, sire.", data)
  174. self.assertNotIn("bacon", data)
  175. @slowTest
  176. def test_get_url_dest_empty(self):
  177. """
  178. cp.get_url with salt:// source given and destination omitted.
  179. """
  180. ret = self.run_function("cp.get_url", ["salt://grail/scene33"])
  181. with salt.utils.files.fopen(ret, "r") as scene:
  182. data = salt.utils.stringutils.to_unicode(scene.read())
  183. self.assertIn("KNIGHT: They're nervous, sire.", data)
  184. self.assertNotIn("bacon", data)
  185. @slowTest
  186. def test_get_url_no_dest(self):
  187. """
  188. cp.get_url with salt:// source given and destination set as None
  189. """
  190. tgt = None
  191. ret = self.run_function("cp.get_url", ["salt://grail/scene33", tgt])
  192. self.assertIn("KNIGHT: They're nervous, sire.", ret)
  193. @slowTest
  194. def test_get_url_nonexistent_source(self):
  195. """
  196. cp.get_url with nonexistent salt:// source given
  197. """
  198. tgt = None
  199. ret = self.run_function("cp.get_url", ["salt://grail/nonexistent_scene", tgt])
  200. self.assertEqual(ret, False)
  201. @slowTest
  202. def test_get_url_to_dir(self):
  203. """
  204. cp.get_url with salt:// source
  205. """
  206. tgt = os.path.join(RUNTIME_VARS.TMP, "")
  207. self.run_function("cp.get_url", ["salt://grail/scene33", tgt])
  208. with salt.utils.files.fopen(tgt + "scene33", "r") as scene:
  209. data = salt.utils.stringutils.to_unicode(scene.read())
  210. self.assertIn("KNIGHT: They're nervous, sire.", data)
  211. self.assertNotIn("bacon", data)
  212. @skipIf(
  213. salt.utils.platform.is_darwin() and six.PY2, "This test hangs on OS X on Py2"
  214. )
  215. @with_tempfile()
  216. @slowTest
  217. def test_get_url_https(self, tgt):
  218. """
  219. cp.get_url with https:// source given
  220. """
  221. self.run_function("cp.get_url", ["https://repo.saltstack.com/index.html", tgt])
  222. with salt.utils.files.fopen(tgt, "r") as instructions:
  223. data = salt.utils.stringutils.to_unicode(instructions.read())
  224. self.assertIn("Bootstrap", data)
  225. self.assertIn("Debian", data)
  226. self.assertIn("Windows", data)
  227. self.assertNotIn("AYBABTU", data)
  228. @skipIf(
  229. salt.utils.platform.is_darwin() and six.PY2, "This test hangs on OS X on Py2"
  230. )
  231. @slowTest
  232. def test_get_url_https_dest_empty(self):
  233. """
  234. cp.get_url with https:// source given and destination omitted.
  235. """
  236. ret = self.run_function("cp.get_url", ["https://repo.saltstack.com/index.html"])
  237. with salt.utils.files.fopen(ret, "r") as instructions:
  238. data = salt.utils.stringutils.to_unicode(instructions.read())
  239. self.assertIn("Bootstrap", data)
  240. self.assertIn("Debian", data)
  241. self.assertIn("Windows", data)
  242. self.assertNotIn("AYBABTU", data)
  243. @skipIf(
  244. salt.utils.platform.is_darwin() and six.PY2, "This test hangs on OS X on Py2"
  245. )
  246. @slowTest
  247. def test_get_url_https_no_dest(self):
  248. """
  249. cp.get_url with https:// source given and destination set as None
  250. """
  251. timeout = 500
  252. start = time.time()
  253. sleep = 5
  254. tgt = None
  255. while time.time() - start <= timeout:
  256. ret = self.run_function(
  257. "cp.get_url", ["https://repo.saltstack.com/index.html", tgt]
  258. )
  259. if ret.find("HTTP 599") == -1:
  260. break
  261. time.sleep(sleep)
  262. if ret.find("HTTP 599") != -1:
  263. raise Exception("https://repo.saltstack.com/index.html returned 599 error")
  264. self.assertIn("Bootstrap", ret)
  265. self.assertIn("Debian", ret)
  266. self.assertIn("Windows", ret)
  267. self.assertNotIn("AYBABTU", ret)
  268. @slowTest
  269. def test_get_url_file(self):
  270. """
  271. cp.get_url with file:// source given
  272. """
  273. tgt = ""
  274. src = os.path.join("file://", RUNTIME_VARS.FILES, "file", "base", "file.big")
  275. ret = self.run_function("cp.get_url", [src, tgt])
  276. with salt.utils.files.fopen(ret, "r") as scene:
  277. data = salt.utils.stringutils.to_unicode(scene.read())
  278. self.assertIn("KNIGHT: They're nervous, sire.", data)
  279. self.assertNotIn("bacon", data)
  280. @slowTest
  281. def test_get_url_file_no_dest(self):
  282. """
  283. cp.get_url with file:// source given and destination set as None
  284. """
  285. tgt = None
  286. src = os.path.join("file://", RUNTIME_VARS.FILES, "file", "base", "file.big")
  287. ret = self.run_function("cp.get_url", [src, tgt])
  288. self.assertIn("KNIGHT: They're nervous, sire.", ret)
  289. self.assertNotIn("bacon", ret)
  290. @with_tempfile()
  291. @slowTest
  292. def test_get_url_ftp(self, tgt):
  293. """
  294. cp.get_url with https:// source given
  295. """
  296. self.run_function(
  297. "cp.get_url",
  298. [
  299. "ftp://ftp.freebsd.org/pub/FreeBSD/releases/amd64/amd64/12.0-RELEASE/MANIFEST",
  300. tgt,
  301. ],
  302. )
  303. with salt.utils.files.fopen(tgt, "r") as instructions:
  304. data = salt.utils.stringutils.to_unicode(instructions.read())
  305. self.assertIn("Base system", data)
  306. # cp.get_file_str tests
  307. @slowTest
  308. def test_get_file_str_salt(self):
  309. """
  310. cp.get_file_str with salt:// source given
  311. """
  312. src = "salt://grail/scene33"
  313. ret = self.run_function("cp.get_file_str", [src])
  314. self.assertIn("KNIGHT: They're nervous, sire.", ret)
  315. @slowTest
  316. def test_get_file_str_nonexistent_source(self):
  317. """
  318. cp.get_file_str with nonexistent salt:// source given
  319. """
  320. src = "salt://grail/nonexistent_scene"
  321. ret = self.run_function("cp.get_file_str", [src])
  322. self.assertEqual(ret, False)
  323. @skipIf(
  324. salt.utils.platform.is_darwin() and six.PY2, "This test hangs on OS X on Py2"
  325. )
  326. @slowTest
  327. def test_get_file_str_https(self):
  328. """
  329. cp.get_file_str with https:// source given
  330. """
  331. src = "https://repo.saltstack.com/index.html"
  332. ret = self.run_function("cp.get_file_str", [src])
  333. self.assertIn("Bootstrap", ret)
  334. self.assertIn("Debian", ret)
  335. self.assertIn("Windows", ret)
  336. self.assertNotIn("AYBABTU", ret)
  337. @slowTest
  338. def test_get_file_str_local(self):
  339. """
  340. cp.get_file_str with file:// source given
  341. """
  342. src = os.path.join("file://", RUNTIME_VARS.FILES, "file", "base", "file.big")
  343. ret = self.run_function("cp.get_file_str", [src])
  344. self.assertIn("KNIGHT: They're nervous, sire.", ret)
  345. self.assertNotIn("bacon", ret)
  346. # caching tests
  347. @slowTest
  348. def test_cache_file(self):
  349. """
  350. cp.cache_file
  351. """
  352. ret = self.run_function("cp.cache_file", ["salt://grail/scene33"])
  353. with salt.utils.files.fopen(ret, "r") as scene:
  354. data = salt.utils.stringutils.to_unicode(scene.read())
  355. self.assertIn("KNIGHT: They're nervous, sire.", data)
  356. self.assertNotIn("bacon", data)
  357. @slowTest
  358. def test_cache_files(self):
  359. """
  360. cp.cache_files
  361. """
  362. ret = self.run_function(
  363. "cp.cache_files", [["salt://grail/scene33", "salt://grail/36/scene"]]
  364. )
  365. for path in ret:
  366. with salt.utils.files.fopen(path, "r") as scene:
  367. data = salt.utils.stringutils.to_unicode(scene.read())
  368. self.assertIn("ARTHUR:", data)
  369. self.assertNotIn("bacon", data)
  370. @with_tempfile()
  371. @slowTest
  372. def test_cache_master(self, tgt):
  373. """
  374. cp.cache_master
  375. """
  376. ret = self.run_function("cp.cache_master", [tgt],)
  377. for path in ret:
  378. self.assertTrue(os.path.exists(path))
  379. @slowTest
  380. def test_cache_local_file(self):
  381. """
  382. cp.cache_local_file
  383. """
  384. src = os.path.join(RUNTIME_VARS.TMP, "random")
  385. with salt.utils.files.fopen(src, "w+") as fn_:
  386. fn_.write(salt.utils.stringutils.to_str("foo"))
  387. ret = self.run_function("cp.cache_local_file", [src])
  388. with salt.utils.files.fopen(ret, "r") as cp_:
  389. self.assertEqual(salt.utils.stringutils.to_unicode(cp_.read()), "foo")
  390. @skipIf(not salt.utils.path.which("nginx"), "nginx not installed")
  391. @skip_if_not_root
  392. @slowTest
  393. def test_cache_remote_file(self):
  394. """
  395. cp.cache_file
  396. """
  397. nginx_port = get_unused_localhost_port()
  398. url_prefix = "http://localhost:{0}/".format(nginx_port)
  399. temp_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  400. self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True)
  401. nginx_root_dir = os.path.join(temp_dir, "root")
  402. nginx_conf_dir = os.path.join(temp_dir, "conf")
  403. nginx_conf = os.path.join(nginx_conf_dir, "nginx.conf")
  404. nginx_pidfile = os.path.join(nginx_conf_dir, "nginx.pid")
  405. file_contents = "Hello world!"
  406. for dirname in (nginx_root_dir, nginx_conf_dir):
  407. os.makedirs(dirname)
  408. # Write the temp file
  409. with salt.utils.files.fopen(
  410. os.path.join(nginx_root_dir, "actual_file"), "w"
  411. ) as fp_:
  412. fp_.write(salt.utils.stringutils.to_str(file_contents))
  413. # Write the nginx config
  414. with salt.utils.files.fopen(nginx_conf, "w") as fp_:
  415. fp_.write(
  416. textwrap.dedent(
  417. salt.utils.stringutils.to_str(
  418. """\
  419. user root;
  420. worker_processes 1;
  421. error_log {nginx_conf_dir}/server_error.log;
  422. pid {nginx_pidfile};
  423. events {{
  424. worker_connections 1024;
  425. }}
  426. http {{
  427. include /etc/nginx/mime.types;
  428. default_type application/octet-stream;
  429. access_log {nginx_conf_dir}/access.log;
  430. error_log {nginx_conf_dir}/error.log;
  431. server {{
  432. listen {nginx_port} default_server;
  433. server_name cachefile.local;
  434. root {nginx_root_dir};
  435. location ~ ^/301$ {{
  436. return 301 /actual_file;
  437. }}
  438. location ~ ^/302$ {{
  439. return 302 /actual_file;
  440. }}
  441. }}
  442. }}""".format(
  443. **locals()
  444. )
  445. )
  446. )
  447. )
  448. self.run_function("cmd.run", [["nginx", "-c", nginx_conf]], python_shell=False)
  449. with salt.utils.files.fopen(nginx_pidfile) as fp_:
  450. nginx_pid = int(fp_.read().strip())
  451. nginx_proc = psutil.Process(pid=nginx_pid)
  452. self.addCleanup(nginx_proc.send_signal, signal.SIGQUIT)
  453. for code in ("", "301", "302"):
  454. url = url_prefix + (code or "actual_file")
  455. log.debug("attempting to cache %s", url)
  456. ret = self.run_function("cp.cache_file", [url])
  457. self.assertTrue(ret)
  458. with salt.utils.files.fopen(ret) as fp_:
  459. cached_contents = salt.utils.stringutils.to_unicode(fp_.read())
  460. self.assertEqual(cached_contents, file_contents)
  461. @slowTest
  462. def test_list_states(self):
  463. """
  464. cp.list_states
  465. """
  466. ret = self.run_function("cp.list_states",)
  467. self.assertIn("core", ret)
  468. self.assertIn("top", ret)
  469. @slowTest
  470. def test_list_minion(self):
  471. """
  472. cp.list_minion
  473. """
  474. self.run_function("cp.cache_file", ["salt://grail/scene33"])
  475. ret = self.run_function("cp.list_minion")
  476. found = False
  477. search = "grail/scene33"
  478. if salt.utils.platform.is_windows():
  479. search = r"grail\scene33"
  480. for path in ret:
  481. if search in path:
  482. found = True
  483. break
  484. self.assertTrue(found)
  485. @slowTest
  486. def test_is_cached(self):
  487. """
  488. cp.is_cached
  489. """
  490. self.run_function("cp.cache_file", ["salt://grail/scene33"])
  491. ret1 = self.run_function("cp.is_cached", ["salt://grail/scene33"])
  492. self.assertTrue(ret1)
  493. ret2 = self.run_function("cp.is_cached", ["salt://fasldkgj/poicxzbn"])
  494. self.assertFalse(ret2)
  495. @slowTest
  496. def test_hash_file(self):
  497. """
  498. cp.hash_file
  499. """
  500. sha256_hash = self.run_function("cp.hash_file", ["salt://grail/scene33"])
  501. path = self.run_function("cp.cache_file", ["salt://grail/scene33"])
  502. with salt.utils.files.fopen(path, "rb") as fn_:
  503. data = fn_.read()
  504. self.assertEqual(sha256_hash["hsum"], hashlib.sha256(data).hexdigest())
  505. @with_tempfile()
  506. @slowTest
  507. def test_get_file_from_env_predefined(self, tgt):
  508. """
  509. cp.get_file
  510. """
  511. tgt = os.path.join(RUNTIME_VARS.TMP, "cheese")
  512. try:
  513. self.run_function("cp.get_file", ["salt://cheese", tgt])
  514. with salt.utils.files.fopen(tgt, "r") as cheese:
  515. data = salt.utils.stringutils.to_unicode(cheese.read())
  516. self.assertIn("Gromit", data)
  517. self.assertNotIn("Comte", data)
  518. finally:
  519. os.unlink(tgt)
  520. @with_tempfile()
  521. @slowTest
  522. def test_get_file_from_env_in_url(self, tgt):
  523. tgt = os.path.join(RUNTIME_VARS.TMP, "cheese")
  524. try:
  525. self.run_function("cp.get_file", ["salt://cheese?saltenv=prod", tgt])
  526. with salt.utils.files.fopen(tgt, "r") as cheese:
  527. data = salt.utils.stringutils.to_unicode(cheese.read())
  528. self.assertIn("Gromit", data)
  529. self.assertIn("Comte", data)
  530. finally:
  531. os.unlink(tgt)
  532. @slowTest
  533. def test_push(self):
  534. log_to_xfer = os.path.join(RUNTIME_VARS.TMP, uuid.uuid4().hex)
  535. open(log_to_xfer, "w").close() # pylint: disable=resource-leakage
  536. try:
  537. self.run_function("cp.push", [log_to_xfer])
  538. tgt_cache_file = os.path.join(
  539. RUNTIME_VARS.TMP,
  540. "master-minion-root",
  541. "cache",
  542. "minions",
  543. "minion",
  544. "files",
  545. RUNTIME_VARS.TMP,
  546. log_to_xfer,
  547. )
  548. self.assertTrue(
  549. os.path.isfile(tgt_cache_file), "File was not cached on the master"
  550. )
  551. finally:
  552. os.unlink(tgt_cache_file)
  553. @slowTest
  554. def test_envs(self):
  555. self.assertEqual(sorted(self.run_function("cp.envs")), sorted(["base", "prod"]))