test_templates.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. """
  2. Unit tests for salt.utils.templates.py
  3. """
  4. import logging
  5. import os
  6. import sys
  7. from pathlib import PurePath, PurePosixPath
  8. import salt.utils.files
  9. import salt.utils.templates
  10. from tests.support import mock
  11. from tests.support.helpers import with_tempdir
  12. from tests.support.unit import TestCase, skipIf
  13. try:
  14. import Cheetah as _
  15. HAS_CHEETAH = True
  16. except ImportError:
  17. HAS_CHEETAH = False
  18. log = logging.getLogger(__name__)
  19. class RenderTestCase(TestCase):
  20. def setUp(self):
  21. # Default context for salt.utils.templates.render_*_tmpl to work
  22. self.context = {
  23. "opts": {"cachedir": "/D", "__cli": "salt"},
  24. "saltenv": None,
  25. }
  26. ### Tests for Jinja (whitespace-friendly)
  27. def test_render_jinja_sanity(self):
  28. tmpl = """OK"""
  29. res = salt.utils.templates.render_jinja_tmpl(tmpl, dict(self.context))
  30. self.assertEqual(res, "OK")
  31. def test_render_jinja_evaluate(self):
  32. tmpl = """{{ "OK" }}"""
  33. res = salt.utils.templates.render_jinja_tmpl(tmpl, dict(self.context))
  34. self.assertEqual(res, "OK")
  35. def test_render_jinja_evaluate_multi(self):
  36. tmpl = """{% if 1 -%}OK{%- endif %}"""
  37. res = salt.utils.templates.render_jinja_tmpl(tmpl, dict(self.context))
  38. self.assertEqual(res, "OK")
  39. def test_render_jinja_variable(self):
  40. tmpl = """{{ var }}"""
  41. ctx = dict(self.context)
  42. ctx["var"] = "OK"
  43. res = salt.utils.templates.render_jinja_tmpl(tmpl, ctx)
  44. self.assertEqual(res, "OK")
  45. ### Tests for mako template
  46. def test_render_mako_sanity(self):
  47. tmpl = """OK"""
  48. res = salt.utils.templates.render_mako_tmpl(tmpl, dict(self.context))
  49. self.assertEqual(res, "OK")
  50. def test_render_mako_evaluate(self):
  51. tmpl = """${ "OK" }"""
  52. res = salt.utils.templates.render_mako_tmpl(tmpl, dict(self.context))
  53. self.assertEqual(res, "OK")
  54. def test_render_mako_evaluate_multi(self):
  55. tmpl = """
  56. % if 1:
  57. OK
  58. % endif
  59. """
  60. res = salt.utils.templates.render_mako_tmpl(tmpl, dict(self.context))
  61. stripped = res.strip()
  62. self.assertEqual(stripped, "OK")
  63. def test_render_mako_variable(self):
  64. tmpl = """${ var }"""
  65. ctx = dict(self.context)
  66. ctx["var"] = "OK"
  67. res = salt.utils.templates.render_mako_tmpl(tmpl, ctx)
  68. self.assertEqual(res, "OK")
  69. ### Tests for wempy template
  70. @skipIf(
  71. sys.version_info > (3,),
  72. "The wempy module is currently unsupported under Python3",
  73. )
  74. def test_render_wempy_sanity(self):
  75. tmpl = """OK"""
  76. res = salt.utils.templates.render_wempy_tmpl(tmpl, dict(self.context))
  77. self.assertEqual(res, "OK")
  78. @skipIf(
  79. sys.version_info > (3,),
  80. "The wempy module is currently unsupported under Python3",
  81. )
  82. def test_render_wempy_evaluate(self):
  83. tmpl = """{{="OK"}}"""
  84. res = salt.utils.templates.render_wempy_tmpl(tmpl, dict(self.context))
  85. self.assertEqual(res, "OK")
  86. @skipIf(
  87. sys.version_info > (3,),
  88. "The wempy module is currently unsupported under Python3",
  89. )
  90. def test_render_wempy_evaluate_multi(self):
  91. tmpl = """{{if 1:}}OK{{pass}}"""
  92. res = salt.utils.templates.render_wempy_tmpl(tmpl, dict(self.context))
  93. self.assertEqual(res, "OK")
  94. @skipIf(
  95. sys.version_info > (3,),
  96. "The wempy module is currently unsupported under Python3",
  97. )
  98. def test_render_wempy_variable(self):
  99. tmpl = """{{=var}}"""
  100. ctx = dict(self.context)
  101. ctx["var"] = "OK"
  102. res = salt.utils.templates.render_wempy_tmpl(tmpl, ctx)
  103. self.assertEqual(res, "OK")
  104. ### Tests for genshi template (xml-based)
  105. def test_render_genshi_sanity(self):
  106. tmpl = """<RU>OK</RU>"""
  107. res = salt.utils.templates.render_genshi_tmpl(tmpl, dict(self.context))
  108. self.assertEqual(res, "<RU>OK</RU>")
  109. def test_render_genshi_evaluate(self):
  110. tmpl = """<RU>${ "OK" }</RU>"""
  111. res = salt.utils.templates.render_genshi_tmpl(tmpl, dict(self.context))
  112. self.assertEqual(res, "<RU>OK</RU>")
  113. def test_render_genshi_evaluate_condition(self):
  114. tmpl = """<RU xmlns:py="http://genshi.edgewall.org/" py:if="1">OK</RU>"""
  115. res = salt.utils.templates.render_genshi_tmpl(tmpl, dict(self.context))
  116. self.assertEqual(res, "<RU>OK</RU>")
  117. def test_render_genshi_variable(self):
  118. tmpl = """<RU>$var</RU>"""
  119. ctx = dict(self.context)
  120. ctx["var"] = "OK"
  121. res = salt.utils.templates.render_genshi_tmpl(tmpl, ctx)
  122. self.assertEqual(res, "<RU>OK</RU>")
  123. def test_render_genshi_variable_replace(self):
  124. tmpl = """<RU xmlns:py="http://genshi.edgewall.org/" py:content="var">not ok</RU>"""
  125. ctx = dict(self.context)
  126. ctx["var"] = "OK"
  127. res = salt.utils.templates.render_genshi_tmpl(tmpl, ctx)
  128. self.assertEqual(res, "<RU>OK</RU>")
  129. ### Tests for cheetah template (line-oriented and xml-friendly)
  130. @skipIf(not HAS_CHEETAH, "The Cheetah Python module is missing.")
  131. def test_render_cheetah_sanity(self):
  132. tmpl = """OK"""
  133. res = salt.utils.templates.render_cheetah_tmpl(tmpl, dict(self.context))
  134. self.assertEqual(res, "OK")
  135. @skipIf(not HAS_CHEETAH, "The Cheetah Python module is missing.")
  136. def test_render_cheetah_evaluate(self):
  137. tmpl = """<%="OK"%>"""
  138. res = salt.utils.templates.render_cheetah_tmpl(tmpl, dict(self.context))
  139. self.assertEqual(res, "OK")
  140. @skipIf(not HAS_CHEETAH, "The Cheetah Python module is missing.")
  141. def test_render_cheetah_evaluate_xml(self):
  142. tmpl = """
  143. <% if 1: %>
  144. OK
  145. <% pass %>
  146. """
  147. res = salt.utils.templates.render_cheetah_tmpl(tmpl, dict(self.context))
  148. stripped = res.strip()
  149. self.assertEqual(stripped, "OK")
  150. @skipIf(not HAS_CHEETAH, "The Cheetah Python module is missing.")
  151. def test_render_cheetah_evaluate_text(self):
  152. tmpl = """
  153. #if 1
  154. OK
  155. #end if
  156. """
  157. res = salt.utils.templates.render_cheetah_tmpl(tmpl, dict(self.context))
  158. stripped = res.strip()
  159. self.assertEqual(stripped, "OK")
  160. @skipIf(not HAS_CHEETAH, "The Cheetah Python module is missing.")
  161. def test_render_cheetah_variable(self):
  162. tmpl = """$var"""
  163. ctx = dict(self.context)
  164. ctx["var"] = "OK"
  165. res = salt.utils.templates.render_cheetah_tmpl(tmpl, ctx)
  166. self.assertEqual(res.strip(), "OK")
  167. class MockRender:
  168. def __call__(self, tplstr, context, tmplpath=None):
  169. self.tplstr = tplstr
  170. self.context = context
  171. self.tmplpath = tmplpath
  172. return tplstr
  173. class WrapRenderTestCase(TestCase):
  174. def assertDictContainsAll(self, actual, **expected):
  175. """ Make sure dictionary contains at least all expected values"""
  176. actual = {key: actual[key] for key in expected if key in actual}
  177. self.assertEqual(expected, actual)
  178. def _test_generated_sls_context(self, tmplpath, sls, **expected):
  179. """ Generic SLS Context Test"""
  180. # DeNormalize tmplpath
  181. tmplpath = str(PurePath(PurePosixPath(tmplpath)))
  182. if tmplpath.startswith("\\"):
  183. tmplpath = "C:{}".format(tmplpath)
  184. expected["tplpath"] = tmplpath
  185. actual = salt.utils.templates._generate_sls_context(tmplpath, sls)
  186. self.assertDictContainsAll(actual, **expected)
  187. @mock.patch("salt.utils.templates.generate_sls_context")
  188. @with_tempdir()
  189. def test_sls_context_call(self, tempdir, generate_sls_context):
  190. """ Check that generate_sls_context is called with proper parameters"""
  191. sls = "foo.bar"
  192. tmplpath = "/tmp/foo/bar.sls"
  193. slsfile = os.path.join(tempdir, "foo")
  194. with salt.utils.files.fopen(slsfile, "w") as fp:
  195. fp.write("{{ slspath }}")
  196. context = {"opts": {}, "saltenv": "base", "sls": sls}
  197. render = MockRender()
  198. wrapped = salt.utils.templates.wrap_tmpl_func(render)
  199. res = wrapped(slsfile, context=context, tmplpath=tmplpath)
  200. generate_sls_context.assert_called_with(tmplpath, sls)
  201. @mock.patch("salt.utils.templates.generate_sls_context")
  202. @with_tempdir()
  203. def test_sls_context_no_call(self, tempdir, generate_sls_context):
  204. """ Check that generate_sls_context is not called if sls is not set"""
  205. sls = "foo.bar"
  206. tmplpath = "/tmp/foo/bar.sls"
  207. slsfile = os.path.join(tempdir, "foo")
  208. with salt.utils.files.fopen(slsfile, "w") as fp:
  209. fp.write("{{ slspath }}")
  210. context = {"opts": {}, "saltenv": "base"}
  211. render = MockRender()
  212. wrapped = salt.utils.templates.wrap_tmpl_func(render)
  213. res = wrapped(slsfile, context=context, tmplpath=tmplpath)
  214. generate_sls_context.assert_not_called()
  215. def test_generate_sls_context__top_level(self):
  216. """ generate_sls_context - top_level Use case"""
  217. self._test_generated_sls_context(
  218. "/tmp/boo.sls",
  219. "boo",
  220. tplfile="boo.sls",
  221. tpldir=".",
  222. tpldot="",
  223. slsdotpath="",
  224. slscolonpath="",
  225. sls_path="",
  226. slspath="",
  227. )
  228. def test_generate_sls_context__one_level_init_implicit(self):
  229. """ generate_sls_context - Basic one level with implicit init.sls """
  230. self._test_generated_sls_context(
  231. "/tmp/foo/init.sls",
  232. "foo",
  233. tplfile="foo/init.sls",
  234. tpldir="foo",
  235. tpldot="foo",
  236. slsdotpath="foo",
  237. slscolonpath="foo",
  238. sls_path="foo",
  239. slspath="foo",
  240. )
  241. def test_generate_sls_context__one_level_init_explicit(self):
  242. """ generate_sls_context - Basic one level with explicit init.sls """
  243. self._test_generated_sls_context(
  244. "/tmp/foo/init.sls",
  245. "foo.init",
  246. tplfile="foo/init.sls",
  247. tpldir="foo",
  248. tpldot="foo",
  249. slsdotpath="foo",
  250. slscolonpath="foo",
  251. sls_path="foo",
  252. slspath="foo",
  253. )
  254. def test_generate_sls_context__one_level(self):
  255. """ generate_sls_context - Basic one level with name"""
  256. self._test_generated_sls_context(
  257. "/tmp/foo/boo.sls",
  258. "foo.boo",
  259. tplfile="foo/boo.sls",
  260. tpldir="foo",
  261. tpldot="foo",
  262. slsdotpath="foo",
  263. slscolonpath="foo",
  264. sls_path="foo",
  265. slspath="foo",
  266. )
  267. def test_generate_sls_context__one_level_repeating(self):
  268. """ generate_sls_context - Basic one level with name same as dir
  269. (Issue #56410)
  270. """
  271. self._test_generated_sls_context(
  272. "/tmp/foo/foo.sls",
  273. "foo.foo",
  274. tplfile="foo/foo.sls",
  275. tpldir="foo",
  276. tpldot="foo",
  277. slsdotpath="foo",
  278. slscolonpath="foo",
  279. sls_path="foo",
  280. slspath="foo",
  281. )
  282. def test_generate_sls_context__two_level_init_implicit(self):
  283. """ generate_sls_context - Basic two level with implicit init.sls """
  284. self._test_generated_sls_context(
  285. "/tmp/foo/bar/init.sls",
  286. "foo.bar",
  287. tplfile="foo/bar/init.sls",
  288. tpldir="foo/bar",
  289. tpldot="foo.bar",
  290. slsdotpath="foo.bar",
  291. slscolonpath="foo:bar",
  292. sls_path="foo_bar",
  293. slspath="foo/bar",
  294. )
  295. def test_generate_sls_context__two_level_init_explicit(self):
  296. """ generate_sls_context - Basic two level with explicit init.sls """
  297. self._test_generated_sls_context(
  298. "/tmp/foo/bar/init.sls",
  299. "foo.bar.init",
  300. tplfile="foo/bar/init.sls",
  301. tpldir="foo/bar",
  302. tpldot="foo.bar",
  303. slsdotpath="foo.bar",
  304. slscolonpath="foo:bar",
  305. sls_path="foo_bar",
  306. slspath="foo/bar",
  307. )
  308. def test_generate_sls_context__two_level(self):
  309. """ generate_sls_context - Basic two level with name"""
  310. self._test_generated_sls_context(
  311. "/tmp/foo/bar/boo.sls",
  312. "foo.bar.boo",
  313. tplfile="foo/bar/boo.sls",
  314. tpldir="foo/bar",
  315. tpldot="foo.bar",
  316. slsdotpath="foo.bar",
  317. slscolonpath="foo:bar",
  318. sls_path="foo_bar",
  319. slspath="foo/bar",
  320. )
  321. def test_generate_sls_context__two_level_repeating(self):
  322. """ generate_sls_context - Basic two level with name same as dir
  323. (Issue #56410)
  324. """
  325. self._test_generated_sls_context(
  326. "/tmp/foo/foo/foo.sls",
  327. "foo.foo.foo",
  328. tplfile="foo/foo/foo.sls",
  329. tpldir="foo/foo",
  330. tpldot="foo.foo",
  331. slsdotpath="foo.foo",
  332. slscolonpath="foo:foo",
  333. sls_path="foo_foo",
  334. slspath="foo/foo",
  335. )
  336. @mock.patch(
  337. "salt.utils.templates._generate_sls_context_legacy", return_value="legacy"
  338. )
  339. @mock.patch("salt.utils.templates._generate_sls_context", return_value="new")
  340. @mock.patch("salt.utils.templates.features.get", return_value=True)
  341. def test_feature_flag_on(self, feature_get, new_impl, legacy_impl):
  342. """ Test feature flag selection with FF on"""
  343. tplpath = "tplpath"
  344. sls = "sls"
  345. self.assertEqual("new", salt.utils.templates.generate_sls_context(tplpath, sls))
  346. new_impl.assert_called_with(tplpath, sls)
  347. legacy_impl.assert_not_called()
  348. @mock.patch(
  349. "salt.utils.templates._generate_sls_context_legacy", return_value="legacy"
  350. )
  351. @mock.patch("salt.utils.templates._generate_sls_context", return_value="new")
  352. @mock.patch("salt.utils.templates.features.get", return_value=False)
  353. def test_feature_flag_off(self, feature_get, new_impl, legacy_impl):
  354. """ Test feature flag selection with FF on"""
  355. tplpath = "tplpath"
  356. sls = "sls"
  357. self.assertEqual(
  358. "legacy", salt.utils.templates.generate_sls_context(tplpath, sls)
  359. )
  360. new_impl.assert_not_called()
  361. legacy_impl.assert_called_with(tplpath, sls)
  362. @skipIf(sys.platform == "win32", "Backslash not possible under windows")
  363. def test_generate_sls_context__backslash_in_path(self):
  364. """ generate_sls_context - Handle backslash in path on non-windows
  365. """
  366. self._test_generated_sls_context(
  367. "/tmp/foo/foo\\foo.sls",
  368. "foo.foo\\foo",
  369. tplfile="foo/foo\\foo.sls",
  370. tpldir="foo",
  371. tpldot="foo",
  372. slsdotpath="foo",
  373. slscolonpath="foo",
  374. sls_path="foo",
  375. slspath="foo",
  376. )