test_zcbuildout.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import, print_function, unicode_literals
  3. import logging
  4. import os
  5. import shutil
  6. import subprocess
  7. import tempfile
  8. import salt.modules.cmdmod as cmd
  9. import salt.modules.virtualenv_mod
  10. import salt.modules.zcbuildout as buildout
  11. import salt.utils.files
  12. import salt.utils.path
  13. import salt.utils.platform
  14. # pylint: disable=import-error,no-name-in-module,redefined-builtin
  15. from salt.ext import six
  16. from salt.ext.six.moves.urllib.error import URLError
  17. from salt.ext.six.moves.urllib.request import urlopen
  18. from tests.support.helpers import patched_environ, requires_network, slowTest
  19. from tests.support.mixins import LoaderModuleMockMixin
  20. from tests.support.runtests import RUNTIME_VARS
  21. from tests.support.unit import TestCase, skipIf
  22. # Import 3rd-party libs
  23. # pylint: enable=import-error,no-name-in-module,redefined-builtin
  24. KNOWN_VIRTUALENV_BINARY_NAMES = (
  25. "virtualenv",
  26. "virtualenv2",
  27. "virtualenv-2.6",
  28. "virtualenv-2.7",
  29. )
  30. # temp workaround since virtualenv pip wheel package does not include
  31. # backports.ssl_match_hostname on windows python2.7
  32. if salt.utils.platform.is_windows() and six.PY2:
  33. KNOWN_VIRTUALENV_BINARY_NAMES = ("c:\\Python27\\Scripts\\virtualenv.EXE",)
  34. BOOT_INIT = {
  35. 1: ["var/ver/1/bootstrap/bootstrap.py"],
  36. 2: ["var/ver/2/bootstrap/bootstrap.py", "b/bootstrap.py"],
  37. }
  38. log = logging.getLogger(__name__)
  39. def download_to(url, dest):
  40. with salt.utils.files.fopen(dest, "wb") as fic:
  41. fic.write(urlopen(url, timeout=10).read())
  42. class Base(TestCase, LoaderModuleMockMixin):
  43. def setup_loader_modules(self):
  44. return {
  45. buildout: {
  46. "__salt__": {
  47. "cmd.run_all": cmd.run_all,
  48. "cmd.run": cmd.run,
  49. "cmd.retcode": cmd.retcode,
  50. }
  51. }
  52. }
  53. @classmethod
  54. def setUpClass(cls):
  55. if not os.path.isdir(RUNTIME_VARS.TMP):
  56. os.makedirs(RUNTIME_VARS.TMP)
  57. cls.root = os.path.join(RUNTIME_VARS.BASE_FILES, "buildout")
  58. cls.rdir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  59. cls.tdir = os.path.join(cls.rdir, "test")
  60. for idx, url in six.iteritems(buildout._URL_VERSIONS):
  61. log.debug("Downloading bootstrap from %s", url)
  62. dest = os.path.join(cls.rdir, "{0}_bootstrap.py".format(idx))
  63. try:
  64. download_to(url, dest)
  65. except URLError:
  66. log.debug("Failed to download %s", url)
  67. # creating a new setuptools install
  68. cls.ppy_st = os.path.join(cls.rdir, "psetuptools")
  69. if salt.utils.platform.is_windows():
  70. cls.bin_st = os.path.join(cls.ppy_st, "Scripts")
  71. cls.py_st = os.path.join(cls.bin_st, "python")
  72. else:
  73. cls.bin_st = os.path.join(cls.ppy_st, "bin")
  74. cls.py_st = os.path.join(cls.bin_st, "python")
  75. # `--no-site-packages` has been deprecated
  76. # https://virtualenv.pypa.io/en/stable/reference/#cmdoption-no-site-packages
  77. subprocess.check_call(
  78. [salt.utils.path.which_bin(KNOWN_VIRTUALENV_BINARY_NAMES), cls.ppy_st]
  79. )
  80. subprocess.check_call(
  81. [os.path.join(cls.bin_st, "pip"), "install", "-U", "setuptools"]
  82. )
  83. # distribute has been merged back in to setuptools as of v0.7. So, no
  84. # need to upgrade distribute, but this seems to be the only way to get
  85. # the binary in the right place
  86. # https://packaging.python.org/key_projects/#setuptools
  87. # Additionally, this part may fail if the certificate store is outdated
  88. # on Windows, as it would be in a fresh installation for example. The
  89. # following commands will fix that. This should be part of the golden
  90. # images. (https://github.com/saltstack/salt-jenkins/pull/1479)
  91. # certutil -generateSSTFromWU roots.sst
  92. # powershell "(Get-ChildItem -Path .\roots.sst) | Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root"
  93. subprocess.check_call(
  94. [os.path.join(cls.bin_st, "easy_install"), "-U", "distribute"]
  95. )
  96. def setUp(self):
  97. if salt.utils.platform.is_darwin and six.PY3:
  98. self.patched_environ = patched_environ(__cleanup__=["__PYVENV_LAUNCHER__"])
  99. self.patched_environ.__enter__()
  100. self.addCleanup(self.patched_environ.__exit__)
  101. super(Base, self).setUp()
  102. self._remove_dir()
  103. shutil.copytree(self.root, self.tdir)
  104. for idx in BOOT_INIT:
  105. path = os.path.join(self.rdir, "{0}_bootstrap.py".format(idx))
  106. for fname in BOOT_INIT[idx]:
  107. shutil.copy2(path, os.path.join(self.tdir, fname))
  108. def tearDown(self):
  109. super(Base, self).tearDown()
  110. self._remove_dir()
  111. def _remove_dir(self):
  112. if os.path.isdir(self.tdir):
  113. shutil.rmtree(self.tdir)
  114. @skipIf(
  115. salt.utils.path.which_bin(KNOWN_VIRTUALENV_BINARY_NAMES) is None,
  116. "The 'virtualenv' packaged needs to be installed",
  117. )
  118. @requires_network()
  119. class BuildoutTestCase(Base):
  120. @slowTest
  121. def test_onlyif_unless(self):
  122. b_dir = os.path.join(self.tdir, "b")
  123. ret = buildout.buildout(b_dir, onlyif=RUNTIME_VARS.SHELL_FALSE_PATH)
  124. self.assertTrue(ret["comment"] == "onlyif condition is false")
  125. self.assertTrue(ret["status"] is True)
  126. ret = buildout.buildout(b_dir, unless=RUNTIME_VARS.SHELL_TRUE_PATH)
  127. self.assertTrue(ret["comment"] == "unless condition is true")
  128. self.assertTrue(ret["status"] is True)
  129. @slowTest
  130. def test_salt_callback(self):
  131. @buildout._salt_callback
  132. def callback1(a, b=1):
  133. for i in buildout.LOG.levels:
  134. getattr(buildout.LOG, i)("{0}bar".format(i[0]))
  135. return "foo"
  136. def callback2(a, b=1):
  137. raise Exception("foo")
  138. # pylint: disable=invalid-sequence-index
  139. ret1 = callback1(1, b=3)
  140. # These lines are throwing pylint errors - disabling for now since we are skipping
  141. # these tests
  142. # self.assertEqual(ret1['status'], True)
  143. # self.assertEqual(ret1['logs_by_level']['warn'], ['wbar'])
  144. # self.assertEqual(ret1['comment'], '')
  145. # These lines are throwing pylint errors - disabling for now since we are skipping
  146. # these tests
  147. # self.assertTrue(
  148. # u''
  149. # u'OUTPUT:\n'
  150. # u'foo\n'
  151. # u''
  152. # in ret1['outlog']
  153. # )
  154. # These lines are throwing pylint errors - disabling for now since we are skipping
  155. # these tests
  156. # self.assertTrue(u'Log summary:\n' in ret1['outlog'])
  157. # These lines are throwing pylint errors - disabling for now since we are skipping
  158. # these tests
  159. # self.assertTrue(
  160. # u'INFO: ibar\n'
  161. # u'WARN: wbar\n'
  162. # u'DEBUG: dbar\n'
  163. # u'ERROR: ebar\n'
  164. # in ret1['outlog']
  165. # )
  166. # These lines are throwing pylint errors - disabling for now since we are skipping
  167. # these tests
  168. # self.assertTrue('by level' in ret1['outlog_by_level'])
  169. # self.assertEqual(ret1['out'], 'foo')
  170. ret2 = buildout._salt_callback(callback2)(2, b=6)
  171. self.assertEqual(ret2["status"], False)
  172. self.assertTrue(ret2["logs_by_level"]["error"][0].startswith("Traceback"))
  173. self.assertTrue(
  174. "We did not get any "
  175. "expectable answer "
  176. "from buildout" in ret2["comment"]
  177. )
  178. self.assertEqual(ret2["out"], None)
  179. for l in buildout.LOG.levels:
  180. self.assertTrue(0 == len(buildout.LOG.by_level[l]))
  181. # pylint: enable=invalid-sequence-index
  182. @slowTest
  183. def test_get_bootstrap_url(self):
  184. for path in [
  185. os.path.join(self.tdir, "var/ver/1/dumppicked"),
  186. os.path.join(self.tdir, "var/ver/1/bootstrap"),
  187. os.path.join(self.tdir, "var/ver/1/versions"),
  188. ]:
  189. self.assertEqual(
  190. buildout._URL_VERSIONS[1],
  191. buildout._get_bootstrap_url(path),
  192. "b1 url for {0}".format(path),
  193. )
  194. for path in [
  195. os.path.join(self.tdir, "/non/existing"),
  196. os.path.join(self.tdir, "var/ver/2/versions"),
  197. os.path.join(self.tdir, "var/ver/2/bootstrap"),
  198. os.path.join(self.tdir, "var/ver/2/default"),
  199. ]:
  200. self.assertEqual(
  201. buildout._URL_VERSIONS[2],
  202. buildout._get_bootstrap_url(path),
  203. "b2 url for {0}".format(path),
  204. )
  205. @slowTest
  206. def test_get_buildout_ver(self):
  207. for path in [
  208. os.path.join(self.tdir, "var/ver/1/dumppicked"),
  209. os.path.join(self.tdir, "var/ver/1/bootstrap"),
  210. os.path.join(self.tdir, "var/ver/1/versions"),
  211. ]:
  212. self.assertEqual(
  213. 1, buildout._get_buildout_ver(path), "1 for {0}".format(path)
  214. )
  215. for path in [
  216. os.path.join(self.tdir, "/non/existing"),
  217. os.path.join(self.tdir, "var/ver/2/versions"),
  218. os.path.join(self.tdir, "var/ver/2/bootstrap"),
  219. os.path.join(self.tdir, "var/ver/2/default"),
  220. ]:
  221. self.assertEqual(
  222. 2, buildout._get_buildout_ver(path), "2 for {0}".format(path)
  223. )
  224. @slowTest
  225. def test_get_bootstrap_content(self):
  226. self.assertEqual(
  227. "",
  228. buildout._get_bootstrap_content(os.path.join(self.tdir, "non", "existing")),
  229. )
  230. self.assertEqual(
  231. "",
  232. buildout._get_bootstrap_content(os.path.join(self.tdir, "var", "tb", "1")),
  233. )
  234. self.assertEqual(
  235. "foo{0}".format(os.linesep),
  236. buildout._get_bootstrap_content(os.path.join(self.tdir, "var", "tb", "2")),
  237. )
  238. @slowTest
  239. def test_logger_clean(self):
  240. buildout.LOG.clear()
  241. # nothing in there
  242. self.assertTrue(
  243. True
  244. not in [len(buildout.LOG.by_level[a]) > 0 for a in buildout.LOG.by_level]
  245. )
  246. buildout.LOG.info("foo")
  247. self.assertTrue(
  248. True in [len(buildout.LOG.by_level[a]) > 0 for a in buildout.LOG.by_level]
  249. )
  250. buildout.LOG.clear()
  251. self.assertTrue(
  252. True
  253. not in [len(buildout.LOG.by_level[a]) > 0 for a in buildout.LOG.by_level]
  254. )
  255. @slowTest
  256. def test_logger_loggers(self):
  257. buildout.LOG.clear()
  258. # nothing in there
  259. for i in buildout.LOG.levels:
  260. getattr(buildout.LOG, i)("foo")
  261. getattr(buildout.LOG, i)("bar")
  262. getattr(buildout.LOG, i)("moo")
  263. self.assertTrue(len(buildout.LOG.by_level[i]) == 3)
  264. self.assertEqual(buildout.LOG.by_level[i][0], "foo")
  265. self.assertEqual(buildout.LOG.by_level[i][-1], "moo")
  266. @slowTest
  267. def test__find_cfgs(self):
  268. result = sorted(
  269. [a.replace(self.root, "") for a in buildout._find_cfgs(self.root)]
  270. )
  271. assertlist = sorted(
  272. [
  273. os.path.join(os.sep, "buildout.cfg"),
  274. os.path.join(os.sep, "c", "buildout.cfg"),
  275. os.path.join(os.sep, "etc", "buildout.cfg"),
  276. os.path.join(os.sep, "e", "buildout.cfg"),
  277. os.path.join(os.sep, "b", "buildout.cfg"),
  278. os.path.join(os.sep, "b", "bdistribute", "buildout.cfg"),
  279. os.path.join(os.sep, "b", "b2", "buildout.cfg"),
  280. os.path.join(os.sep, "foo", "buildout.cfg"),
  281. ]
  282. )
  283. self.assertEqual(result, assertlist)
  284. def skip_test_upgrade_bootstrap(self):
  285. b_dir = os.path.join(self.tdir, "b")
  286. bpy = os.path.join(b_dir, "bootstrap.py")
  287. buildout.upgrade_bootstrap(b_dir)
  288. time1 = os.stat(bpy).st_mtime
  289. with salt.utils.files.fopen(bpy) as fic:
  290. data = fic.read()
  291. self.assertTrue("setdefaulttimeout(2)" in data)
  292. flag = os.path.join(b_dir, ".buildout", "2.updated_bootstrap")
  293. self.assertTrue(os.path.exists(flag))
  294. buildout.upgrade_bootstrap(b_dir, buildout_ver=1)
  295. time2 = os.stat(bpy).st_mtime
  296. with salt.utils.files.fopen(bpy) as fic:
  297. data = fic.read()
  298. self.assertTrue("setdefaulttimeout(2)" in data)
  299. flag = os.path.join(b_dir, ".buildout", "1.updated_bootstrap")
  300. self.assertTrue(os.path.exists(flag))
  301. buildout.upgrade_bootstrap(b_dir, buildout_ver=1)
  302. time3 = os.stat(bpy).st_mtime
  303. self.assertNotEqual(time2, time1)
  304. self.assertEqual(time2, time3)
  305. @skipIf(
  306. salt.utils.path.which_bin(KNOWN_VIRTUALENV_BINARY_NAMES) is None,
  307. "The 'virtualenv' packaged needs to be installed",
  308. )
  309. @requires_network()
  310. class BuildoutOnlineTestCase(Base):
  311. @classmethod
  312. def setUpClass(cls):
  313. super(BuildoutOnlineTestCase, cls).setUpClass()
  314. cls.ppy_dis = os.path.join(cls.rdir, "pdistribute")
  315. cls.ppy_blank = os.path.join(cls.rdir, "pblank")
  316. cls.py_dis = os.path.join(cls.ppy_dis, "bin", "python")
  317. cls.py_blank = os.path.join(cls.ppy_blank, "bin", "python")
  318. # creating a distribute based install
  319. try:
  320. # `--no-site-packages` has been deprecated
  321. # https://virtualenv.pypa.io/en/stable/reference/#cmdoption-no-site-packages
  322. subprocess.check_call(
  323. [
  324. salt.utils.path.which_bin(KNOWN_VIRTUALENV_BINARY_NAMES),
  325. "--no-setuptools",
  326. "--no-pip",
  327. cls.ppy_dis,
  328. ]
  329. )
  330. except subprocess.CalledProcessError:
  331. subprocess.check_call(
  332. [salt.utils.path.which_bin(KNOWN_VIRTUALENV_BINARY_NAMES), cls.ppy.dis]
  333. )
  334. url = (
  335. "https://pypi.python.org/packages/source"
  336. "/d/distribute/distribute-0.6.43.tar.gz"
  337. )
  338. download_to(
  339. url, os.path.join(cls.ppy_dis, "distribute-0.6.43.tar.gz"),
  340. )
  341. subprocess.check_call(
  342. [
  343. "tar",
  344. "-C",
  345. cls.ppy_dis,
  346. "-xzvf",
  347. "{0}/distribute-0.6.43.tar.gz".format(cls.ppy_dis),
  348. ]
  349. )
  350. subprocess.check_call(
  351. [
  352. "{0}/bin/python".format(cls.ppy_dis),
  353. "{0}/distribute-0.6.43/setup.py".format(cls.ppy_dis),
  354. "install",
  355. ]
  356. )
  357. # creating a blank based install
  358. try:
  359. subprocess.check_call(
  360. [
  361. salt.utils.path.which_bin(KNOWN_VIRTUALENV_BINARY_NAMES),
  362. "--no-setuptools",
  363. "--no-pip",
  364. cls.ppy_blank,
  365. ]
  366. )
  367. except subprocess.CalledProcessError:
  368. subprocess.check_call(
  369. [
  370. salt.utils.path.which_bin(KNOWN_VIRTUALENV_BINARY_NAMES),
  371. cls.ppy_blank,
  372. ]
  373. )
  374. @skipIf(True, "TODO this test should probably be fixed")
  375. def test_buildout_bootstrap(self):
  376. b_dir = os.path.join(self.tdir, "b")
  377. bd_dir = os.path.join(self.tdir, "b", "bdistribute")
  378. b2_dir = os.path.join(self.tdir, "b", "b2")
  379. self.assertTrue(buildout._has_old_distribute(self.py_dis))
  380. # this is too hard to check as on debian & other where old
  381. # packages are present (virtualenv), we can't have
  382. # a clean site-packages
  383. # self.assertFalse(buildout._has_old_distribute(self.py_blank))
  384. self.assertFalse(buildout._has_old_distribute(self.py_st))
  385. self.assertFalse(buildout._has_setuptools7(self.py_dis))
  386. self.assertTrue(buildout._has_setuptools7(self.py_st))
  387. self.assertFalse(buildout._has_setuptools7(self.py_blank))
  388. ret = buildout.bootstrap(bd_dir, buildout_ver=1, python=self.py_dis)
  389. comment = ret["outlog"]
  390. self.assertTrue("--distribute" in comment)
  391. self.assertTrue("Generated script" in comment)
  392. ret = buildout.bootstrap(b_dir, buildout_ver=1, python=self.py_blank)
  393. comment = ret["outlog"]
  394. # as we may have old packages, this test the two
  395. # behaviors (failure with old setuptools/distribute)
  396. self.assertTrue(
  397. ("Got " in comment and "Generated script" in comment)
  398. or ("setuptools>=0.7" in comment)
  399. )
  400. ret = buildout.bootstrap(b_dir, buildout_ver=2, python=self.py_blank)
  401. comment = ret["outlog"]
  402. self.assertTrue(
  403. ("setuptools" in comment and "Generated script" in comment)
  404. or ("setuptools>=0.7" in comment)
  405. )
  406. ret = buildout.bootstrap(b_dir, buildout_ver=2, python=self.py_st)
  407. comment = ret["outlog"]
  408. self.assertTrue(
  409. ("setuptools" in comment and "Generated script" in comment)
  410. or ("setuptools>=0.7" in comment)
  411. )
  412. ret = buildout.bootstrap(b2_dir, buildout_ver=2, python=self.py_st)
  413. comment = ret["outlog"]
  414. self.assertTrue(
  415. ("setuptools" in comment and "Creating directory" in comment)
  416. or ("setuptools>=0.7" in comment)
  417. )
  418. @slowTest
  419. def test_run_buildout(self):
  420. if salt.modules.virtualenv_mod.virtualenv_ver(self.ppy_st) >= (20, 0, 0):
  421. self.skipTest(
  422. "Skiping until upstream resolved https://github.com/pypa/virtualenv/issues/1715"
  423. )
  424. b_dir = os.path.join(self.tdir, "b")
  425. ret = buildout.bootstrap(b_dir, buildout_ver=2, python=self.py_st)
  426. self.assertTrue(ret["status"])
  427. ret = buildout.run_buildout(b_dir, parts=["a", "b"])
  428. out = ret["out"]
  429. self.assertTrue("Installing a" in out)
  430. self.assertTrue("Installing b" in out)
  431. @slowTest
  432. def test_buildout(self):
  433. if salt.modules.virtualenv_mod.virtualenv_ver(self.ppy_st) >= (20, 0, 0):
  434. self.skipTest(
  435. "Skiping until upstream resolved https://github.com/pypa/virtualenv/issues/1715"
  436. )
  437. b_dir = os.path.join(self.tdir, "b")
  438. ret = buildout.buildout(b_dir, buildout_ver=2, python=self.py_st)
  439. self.assertTrue(ret["status"])
  440. out = ret["out"]
  441. comment = ret["comment"]
  442. self.assertTrue(ret["status"])
  443. self.assertTrue("Creating directory" in out)
  444. self.assertTrue("Installing a." in out)
  445. self.assertTrue("{0} bootstrap.py".format(self.py_st) in comment)
  446. self.assertTrue("buildout -c buildout.cfg" in comment)
  447. ret = buildout.buildout(
  448. b_dir, parts=["a", "b", "c"], buildout_ver=2, python=self.py_st
  449. )
  450. outlog = ret["outlog"]
  451. out = ret["out"]
  452. comment = ret["comment"]
  453. self.assertTrue("Installing single part: a" in outlog)
  454. self.assertTrue("buildout -c buildout.cfg -N install a" in comment)
  455. self.assertTrue("Installing b." in out)
  456. self.assertTrue("Installing c." in out)
  457. ret = buildout.buildout(
  458. b_dir, parts=["a", "b", "c"], buildout_ver=2, newest=True, python=self.py_st
  459. )
  460. outlog = ret["outlog"]
  461. out = ret["out"]
  462. comment = ret["comment"]
  463. self.assertTrue("buildout -c buildout.cfg -n install a" in comment)
  464. # TODO: Is this test even still needed?
  465. class BuildoutAPITestCase(TestCase):
  466. def test_merge(self):
  467. buildout.LOG.clear()
  468. buildout.LOG.info("àé")
  469. buildout.LOG.info("àé")
  470. buildout.LOG.error("àé")
  471. buildout.LOG.error("àé")
  472. ret1 = buildout._set_status({}, out="éà")
  473. uret1 = buildout._set_status({}, out="éà")
  474. buildout.LOG.clear()
  475. buildout.LOG.info("ççàé")
  476. buildout.LOG.info("ççàé")
  477. buildout.LOG.error("ççàé")
  478. buildout.LOG.error("ççàé")
  479. ret2 = buildout._set_status({}, out="çéà")
  480. uret2 = buildout._set_status({}, out="çéà")
  481. uretm = buildout._merge_statuses([ret1, uret1, ret2, uret2])
  482. for ret in ret1, uret1, ret2, uret2:
  483. out = ret["out"]
  484. if not isinstance(ret["out"], six.text_type):
  485. out = ret["out"].decode("utf-8")
  486. for out in ["àé", "ççàé"]:
  487. self.assertTrue(out in uretm["logs_by_level"]["info"])
  488. self.assertTrue(out in uretm["outlog_by_level"])
  489. def test_setup(self):
  490. buildout.LOG.clear()
  491. buildout.LOG.info("àé")
  492. buildout.LOG.info("àé")
  493. buildout.LOG.error("àé")
  494. buildout.LOG.error("àé")
  495. ret = buildout._set_status({}, out="éà")
  496. uret = buildout._set_status({}, out="éà")
  497. self.assertTrue(ret["outlog"] == uret["outlog"])
  498. self.assertTrue("àé" in uret["outlog_by_level"])