test_state.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: Nicole Thomas <nicole@saltstack.com>
  4. '''
  5. # Import Python libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import os
  8. import shutil
  9. import tempfile
  10. # Import Salt Testing libs
  11. from tests.support.unit import TestCase, skipIf
  12. from tests.support.mock import (
  13. MagicMock,
  14. patch)
  15. from tests.support.mixins import AdaptedConfigurationTestCaseMixin
  16. from tests.support.runtests import RUNTIME_VARS
  17. from tests.support.helpers import with_tempfile
  18. # Import Salt libs
  19. import salt.exceptions
  20. import salt.state
  21. from salt.utils.odict import OrderedDict
  22. from salt.utils.decorators import state as statedecorators
  23. import salt.utils.files
  24. import salt.utils.platform
  25. try:
  26. import pytest
  27. except ImportError as err:
  28. pytest = None
  29. class StateCompilerTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  30. '''
  31. TestCase for the state compiler.
  32. '''
  33. def test_format_log_non_ascii_character(self):
  34. '''
  35. Tests running a non-ascii character through the state.format_log
  36. function. See Issue #33605.
  37. '''
  38. # There is no return to test against as the format_log
  39. # function doesn't return anything. However, we do want
  40. # to make sure that the function doesn't stacktrace when
  41. # called.
  42. ret = {'changes': {'Français': {'old': 'something old',
  43. 'new': 'something new'}},
  44. 'result': True}
  45. salt.state.format_log(ret)
  46. def test_render_error_on_invalid_requisite(self):
  47. '''
  48. Test that the state compiler correctly deliver a rendering
  49. exception when a requisite cannot be resolved
  50. '''
  51. with patch('salt.state.State._gather_pillar') as state_patch:
  52. high_data = {
  53. 'git': OrderedDict([
  54. ('pkg', [
  55. OrderedDict([
  56. ('require', [
  57. OrderedDict([
  58. ('file', OrderedDict(
  59. [('test1', 'test')]))])])]),
  60. 'installed', {'order': 10000}]), ('__sls__', 'issue_35226'), ('__env__', 'base')])}
  61. minion_opts = self.get_temp_config('minion')
  62. minion_opts['pillar'] = {'git': OrderedDict([('test1', 'test')])}
  63. state_obj = salt.state.State(minion_opts)
  64. with self.assertRaises(salt.exceptions.SaltRenderError):
  65. state_obj.call_high(high_data)
  66. def test_verify_onlyif_parse(self):
  67. low_data = {
  68. "onlyif": [
  69. {
  70. "fun": "test.arg",
  71. "args": [
  72. "arg1",
  73. "arg2"
  74. ]
  75. }
  76. ],
  77. "name": "mysql-server-5.7",
  78. "state": "debconf",
  79. "__id__": "set root password",
  80. "fun": "set",
  81. "__env__": "base",
  82. "__sls__": "debconf",
  83. "data": {
  84. "mysql-server/root_password": {
  85. "type": "password",
  86. "value": "temp123"
  87. }
  88. },
  89. "order": 10000
  90. }
  91. expected_result = {'comment': 'onlyif condition is true', 'result': False}
  92. with patch('salt.state.State._gather_pillar') as state_patch:
  93. minion_opts = self.get_temp_config('minion')
  94. state_obj = salt.state.State(minion_opts)
  95. return_result = state_obj._run_check_onlyif(low_data, '')
  96. self.assertEqual(expected_result, return_result)
  97. def test_verify_unless_parse(self):
  98. low_data = {
  99. "unless": [
  100. {
  101. "fun": "test.arg",
  102. "args": [
  103. "arg1",
  104. "arg2"
  105. ]
  106. }
  107. ],
  108. "name": "mysql-server-5.7",
  109. "state": "debconf",
  110. "__id__": "set root password",
  111. "fun": "set",
  112. "__env__": "base",
  113. "__sls__": "debconf",
  114. "data": {
  115. "mysql-server/root_password": {
  116. "type": "password",
  117. "value": "temp123"
  118. }
  119. },
  120. "order": 10000
  121. }
  122. expected_result = {'comment': 'unless condition is true', 'result': True, 'skip_watch': True}
  123. with patch('salt.state.State._gather_pillar') as state_patch:
  124. minion_opts = self.get_temp_config('minion')
  125. state_obj = salt.state.State(minion_opts)
  126. return_result = state_obj._run_check_unless(low_data, '')
  127. self.assertEqual(expected_result, return_result)
  128. def _expand_win_path(self, path):
  129. """
  130. Expand C:/users/admini~1/appdata/local/temp/salt-tests-tmpdir/...
  131. into C:/users/adminitrator/appdata/local/temp/salt-tests-tmpdir/...
  132. to prevent file.search from expanding the "~" with os.path.expanduser
  133. """
  134. if salt.utils.platform.is_windows():
  135. import win32file
  136. return win32file.GetLongPathName(path).replace('\\', '/')
  137. else:
  138. return path
  139. @with_tempfile()
  140. def test_verify_onlyif_parse_slots(self, name):
  141. with salt.utils.files.fopen(name, 'w') as fp:
  142. fp.write('file-contents')
  143. low_data = {
  144. "onlyif": [
  145. {
  146. "fun": "file.search",
  147. "args": [
  148. "__slot__:salt:test.echo({})".format(self._expand_win_path(name)),
  149. ],
  150. "pattern": "__slot__:salt:test.echo(file-contents)",
  151. }
  152. ],
  153. "name": "mysql-server-5.7",
  154. "state": "debconf",
  155. "__id__": "set root password",
  156. "fun": "set",
  157. "__env__": "base",
  158. "__sls__": "debconf",
  159. "data": {
  160. "mysql-server/root_password": {
  161. "type": "password",
  162. "value": "temp123"
  163. }
  164. },
  165. "order": 10000
  166. }
  167. expected_result = {'comment': 'onlyif condition is true', 'result': False}
  168. with patch('salt.state.State._gather_pillar') as state_patch:
  169. minion_opts = self.get_temp_config('minion')
  170. state_obj = salt.state.State(minion_opts)
  171. return_result = state_obj._run_check_onlyif(low_data, '')
  172. self.assertEqual(expected_result, return_result)
  173. @with_tempfile()
  174. def test_verify_unless_parse_slots(self, name):
  175. with salt.utils.files.fopen(name, 'w') as fp:
  176. fp.write('file-contents')
  177. low_data = {
  178. "unless": [
  179. {
  180. "fun": "file.search",
  181. "args": [
  182. "__slot__:salt:test.echo({})".format(self._expand_win_path(name)),
  183. ],
  184. "pattern": "__slot__:salt:test.echo(file-contents)",
  185. }
  186. ],
  187. "name": "mysql-server-5.7",
  188. "state": "debconf",
  189. "__id__": "set root password",
  190. "fun": "set",
  191. "__env__": "base",
  192. "__sls__": "debconf",
  193. "data": {
  194. "mysql-server/root_password": {
  195. "type": "password",
  196. "value": "temp123"
  197. }
  198. },
  199. "order": 10000
  200. }
  201. expected_result = {'comment': 'unless condition is true', 'result': True, 'skip_watch': True}
  202. with patch('salt.state.State._gather_pillar') as state_patch:
  203. minion_opts = self.get_temp_config('minion')
  204. state_obj = salt.state.State(minion_opts)
  205. return_result = state_obj._run_check_unless(low_data, '')
  206. self.assertEqual(expected_result, return_result)
  207. class HighStateTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  208. def setUp(self):
  209. root_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  210. self.state_tree_dir = os.path.join(root_dir, 'state_tree')
  211. cache_dir = os.path.join(root_dir, 'cachedir')
  212. for dpath in (root_dir, self.state_tree_dir, cache_dir):
  213. if not os.path.isdir(dpath):
  214. os.makedirs(dpath)
  215. overrides = {}
  216. overrides['root_dir'] = root_dir
  217. overrides['state_events'] = False
  218. overrides['id'] = 'match'
  219. overrides['file_client'] = 'local'
  220. overrides['file_roots'] = dict(base=[self.state_tree_dir])
  221. overrides['cachedir'] = cache_dir
  222. overrides['test'] = False
  223. self.config = self.get_temp_config('minion', **overrides)
  224. self.addCleanup(delattr, self, 'config')
  225. self.highstate = salt.state.HighState(self.config)
  226. self.addCleanup(delattr, self, 'highstate')
  227. self.highstate.push_active()
  228. def tearDown(self):
  229. self.highstate.pop_active()
  230. def test_top_matches_with_list(self):
  231. top = {'env': {'match': ['state1', 'state2'], 'nomatch': ['state3']}}
  232. matches = self.highstate.top_matches(top)
  233. self.assertEqual(matches, {'env': ['state1', 'state2']})
  234. def test_top_matches_with_string(self):
  235. top = {'env': {'match': 'state1', 'nomatch': 'state2'}}
  236. matches = self.highstate.top_matches(top)
  237. self.assertEqual(matches, {'env': ['state1']})
  238. def test_matches_whitelist(self):
  239. matches = {'env': ['state1', 'state2', 'state3']}
  240. matches = self.highstate.matches_whitelist(matches, ['state2'])
  241. self.assertEqual(matches, {'env': ['state2']})
  242. def test_matches_whitelist_with_string(self):
  243. matches = {'env': ['state1', 'state2', 'state3']}
  244. matches = self.highstate.matches_whitelist(matches,
  245. 'state2,state3')
  246. self.assertEqual(matches, {'env': ['state2', 'state3']})
  247. def test_show_state_usage(self):
  248. # monkey patch sub methods
  249. self.highstate.avail = {
  250. 'base': ['state.a', 'state.b', 'state.c']
  251. }
  252. def verify_tops(*args, **kwargs):
  253. return []
  254. def get_top(*args, **kwargs):
  255. return None
  256. def top_matches(*args, **kwargs):
  257. return {'base': ['state.a', 'state.b']}
  258. self.highstate.verify_tops = verify_tops
  259. self.highstate.get_top = get_top
  260. self.highstate.top_matches = top_matches
  261. # get compile_state_usage() result
  262. state_usage_dict = self.highstate.compile_state_usage()
  263. self.assertEqual(state_usage_dict['base']['count_unused'], 1)
  264. self.assertEqual(state_usage_dict['base']['count_used'], 2)
  265. self.assertEqual(state_usage_dict['base']['count_all'], 3)
  266. self.assertEqual(state_usage_dict['base']['used'], ['state.a', 'state.b'])
  267. self.assertEqual(state_usage_dict['base']['unused'], ['state.c'])
  268. def test_find_sls_ids_with_exclude(self):
  269. '''
  270. See https://github.com/saltstack/salt/issues/47182
  271. '''
  272. sls_dir = 'issue-47182'
  273. shutil.copytree(
  274. os.path.join(RUNTIME_VARS.BASE_FILES, sls_dir),
  275. os.path.join(self.state_tree_dir, sls_dir)
  276. )
  277. shutil.move(
  278. os.path.join(self.state_tree_dir, sls_dir, 'top.sls'),
  279. self.state_tree_dir
  280. )
  281. # Manually compile the high data. We don't have to worry about all of
  282. # the normal error checking we do here since we know that all the SLS
  283. # files exist and there is no whitelist/blacklist being used.
  284. top = self.highstate.get_top() # pylint: disable=assignment-from-none
  285. matches = self.highstate.top_matches(top)
  286. high, _ = self.highstate.render_highstate(matches)
  287. ret = salt.state.find_sls_ids('issue-47182.stateA.newer', high)
  288. self.assertEqual(ret, [('somestuff', 'cmd')])
  289. @skipIf(pytest is None, 'PyTest is missing')
  290. class StateReturnsTestCase(TestCase):
  291. '''
  292. TestCase for code handling state returns.
  293. '''
  294. def test_state_output_check_changes_is_dict(self):
  295. '''
  296. Test that changes key contains a dictionary.
  297. :return:
  298. '''
  299. data = {'changes': []}
  300. out = statedecorators.OutputUnifier('content_check')(lambda: data)()
  301. assert "'Changes' should be a dictionary" in out['comment']
  302. assert not out['result']
  303. def test_state_output_check_return_is_dict(self):
  304. '''
  305. Test for the entire return is a dictionary
  306. :return:
  307. '''
  308. data = ['whatever']
  309. out = statedecorators.OutputUnifier('content_check')(lambda: data)()
  310. assert 'Malformed state return. Data must be a dictionary type' in out['comment']
  311. assert not out['result']
  312. def test_state_output_check_return_has_nrc(self):
  313. '''
  314. Test for name/result/comment keys are inside the return.
  315. :return:
  316. '''
  317. data = {'arbitrary': 'data', 'changes': {}}
  318. out = statedecorators.OutputUnifier('content_check')(lambda: data)()
  319. assert ' The following keys were not present in the state return: name, result, comment' in out['comment']
  320. assert not out['result']
  321. def test_state_output_unifier_comment_is_not_list(self):
  322. '''
  323. Test for output is unified so the comment is converted to a multi-line string
  324. :return:
  325. '''
  326. data = {'comment': ['data', 'in', 'the', 'list'], 'changes': {}, 'name': None, 'result': 'fantastic!'}
  327. expected = {'comment': 'data\nin\nthe\nlist', 'changes': {}, 'name': None, 'result': True}
  328. assert statedecorators.OutputUnifier('unify')(lambda: data)() == expected
  329. data = {'comment': ['data', 'in', 'the', 'list'], 'changes': {}, 'name': None, 'result': None}
  330. expected = 'data\nin\nthe\nlist'
  331. assert statedecorators.OutputUnifier('unify')(lambda: data)()['comment'] == expected
  332. def test_state_output_unifier_result_converted_to_true(self):
  333. '''
  334. Test for output is unified so the result is converted to True
  335. :return:
  336. '''
  337. data = {'comment': ['data', 'in', 'the', 'list'], 'changes': {}, 'name': None, 'result': 'Fantastic'}
  338. assert statedecorators.OutputUnifier('unify')(lambda: data)()['result'] is True
  339. def test_state_output_unifier_result_converted_to_false(self):
  340. '''
  341. Test for output is unified so the result is converted to False
  342. :return:
  343. '''
  344. data = {'comment': ['data', 'in', 'the', 'list'], 'changes': {}, 'name': None, 'result': ''}
  345. assert statedecorators.OutputUnifier('unify')(lambda: data)()['result'] is False
  346. class StateFormatSlotsTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  347. '''
  348. TestCase for code handling slots
  349. '''
  350. def setUp(self):
  351. with patch('salt.state.State._gather_pillar'):
  352. minion_opts = self.get_temp_config('minion')
  353. self.state_obj = salt.state.State(minion_opts)
  354. def test_format_slots_no_slots(self):
  355. '''
  356. Test the format slots keeps data without slots untouched.
  357. '''
  358. cdata = {
  359. 'args': [
  360. 'arg',
  361. ],
  362. 'kwargs': {
  363. 'key': 'val',
  364. }
  365. }
  366. self.state_obj.format_slots(cdata)
  367. self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'val'}})
  368. def test_format_slots_arg(self):
  369. '''
  370. Test the format slots is calling a slot specified in args with corresponding arguments.
  371. '''
  372. cdata = {
  373. 'args': [
  374. '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)',
  375. ],
  376. 'kwargs': {
  377. 'key': 'val',
  378. }
  379. }
  380. mock = MagicMock(return_value='fun_return')
  381. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  382. self.state_obj.format_slots(cdata)
  383. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  384. self.assertEqual(cdata, {'args': ['fun_return'], 'kwargs': {'key': 'val'}})
  385. def test_format_slots_dict_arg(self):
  386. '''
  387. Test the format slots is calling a slot specified in dict arg.
  388. '''
  389. cdata = {
  390. 'args': [
  391. {'subarg': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)'},
  392. ],
  393. 'kwargs': {
  394. 'key': 'val',
  395. }
  396. }
  397. mock = MagicMock(return_value='fun_return')
  398. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  399. self.state_obj.format_slots(cdata)
  400. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  401. self.assertEqual(cdata, {'args': [{'subarg': 'fun_return'}], 'kwargs': {'key': 'val'}})
  402. def test_format_slots_listdict_arg(self):
  403. '''
  404. Test the format slots is calling a slot specified in list containing a dict.
  405. '''
  406. cdata = {
  407. 'args': [[
  408. {'subarg': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)'},
  409. ]],
  410. 'kwargs': {
  411. 'key': 'val',
  412. }
  413. }
  414. mock = MagicMock(return_value='fun_return')
  415. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  416. self.state_obj.format_slots(cdata)
  417. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  418. self.assertEqual(cdata, {'args': [[{'subarg': 'fun_return'}]], 'kwargs': {'key': 'val'}})
  419. def test_format_slots_liststr_arg(self):
  420. '''
  421. Test the format slots is calling a slot specified in list containing a dict.
  422. '''
  423. cdata = {
  424. 'args': [[
  425. '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)',
  426. ]],
  427. 'kwargs': {
  428. 'key': 'val',
  429. }
  430. }
  431. mock = MagicMock(return_value='fun_return')
  432. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  433. self.state_obj.format_slots(cdata)
  434. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  435. self.assertEqual(cdata, {'args': [['fun_return']], 'kwargs': {'key': 'val'}})
  436. def test_format_slots_kwarg(self):
  437. '''
  438. Test the format slots is calling a slot specified in kwargs with corresponding arguments.
  439. '''
  440. cdata = {
  441. 'args': [
  442. 'arg',
  443. ],
  444. 'kwargs': {
  445. 'key': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)',
  446. }
  447. }
  448. mock = MagicMock(return_value='fun_return')
  449. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  450. self.state_obj.format_slots(cdata)
  451. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  452. self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'fun_return'}})
  453. def test_format_slots_multi(self):
  454. '''
  455. Test the format slots is calling all slots with corresponding arguments when multiple slots
  456. specified.
  457. '''
  458. cdata = {
  459. 'args': [
  460. '__slot__:salt:test_mod.fun_a(a_arg, a_key=a_kwarg)',
  461. '__slot__:salt:test_mod.fun_b(b_arg, b_key=b_kwarg)',
  462. ],
  463. 'kwargs': {
  464. 'kw_key_1': '__slot__:salt:test_mod.fun_c(c_arg, c_key=c_kwarg)',
  465. 'kw_key_2': '__slot__:salt:test_mod.fun_d(d_arg, d_key=d_kwarg)',
  466. }
  467. }
  468. mock_a = MagicMock(return_value='fun_a_return')
  469. mock_b = MagicMock(return_value='fun_b_return')
  470. mock_c = MagicMock(return_value='fun_c_return')
  471. mock_d = MagicMock(return_value='fun_d_return')
  472. with patch.dict(self.state_obj.functions, {'test_mod.fun_a': mock_a,
  473. 'test_mod.fun_b': mock_b,
  474. 'test_mod.fun_c': mock_c,
  475. 'test_mod.fun_d': mock_d}):
  476. self.state_obj.format_slots(cdata)
  477. mock_a.assert_called_once_with('a_arg', a_key='a_kwarg')
  478. mock_b.assert_called_once_with('b_arg', b_key='b_kwarg')
  479. mock_c.assert_called_once_with('c_arg', c_key='c_kwarg')
  480. mock_d.assert_called_once_with('d_arg', d_key='d_kwarg')
  481. self.assertEqual(cdata, {'args': ['fun_a_return',
  482. 'fun_b_return'],
  483. 'kwargs': {'kw_key_1': 'fun_c_return',
  484. 'kw_key_2': 'fun_d_return'}})
  485. def test_format_slots_malformed(self):
  486. '''
  487. Test the format slots keeps malformed slots untouched.
  488. '''
  489. sls_data = {
  490. 'args': [
  491. '__slot__:NOT_SUPPORTED:not.called()',
  492. '__slot__:salt:not.called(',
  493. '__slot__:salt:',
  494. '__slot__:salt',
  495. '__slot__:',
  496. '__slot__',
  497. ],
  498. 'kwargs': {
  499. 'key3': '__slot__:NOT_SUPPORTED:not.called()',
  500. 'key4': '__slot__:salt:not.called(',
  501. 'key5': '__slot__:salt:',
  502. 'key6': '__slot__:salt',
  503. 'key7': '__slot__:',
  504. 'key8': '__slot__',
  505. }
  506. }
  507. cdata = sls_data.copy()
  508. mock = MagicMock(return_value='return')
  509. with patch.dict(self.state_obj.functions, {'not.called': mock}):
  510. self.state_obj.format_slots(cdata)
  511. mock.assert_not_called()
  512. self.assertEqual(cdata, sls_data)
  513. def test_slot_traverse_dict(self):
  514. '''
  515. Test the slot parsing of dict response.
  516. '''
  517. cdata = {
  518. 'args': [
  519. 'arg',
  520. ],
  521. 'kwargs': {
  522. 'key': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val).key1',
  523. }
  524. }
  525. return_data = {'key1': 'value1'}
  526. mock = MagicMock(return_value=return_data)
  527. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  528. self.state_obj.format_slots(cdata)
  529. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  530. self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'value1'}})
  531. def test_slot_append(self):
  532. '''
  533. Test the slot parsing of dict response.
  534. '''
  535. cdata = {
  536. 'args': [
  537. 'arg',
  538. ],
  539. 'kwargs': {
  540. 'key': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val).key1 ~ thing~',
  541. }
  542. }
  543. return_data = {'key1': 'value1'}
  544. mock = MagicMock(return_value=return_data)
  545. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  546. self.state_obj.format_slots(cdata)
  547. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  548. self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'value1thing~'}})