test_load.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. import io
  2. import sys
  3. import pytest
  4. from flexmock import flexmock
  5. from borgmatic.config import load as module
  6. def test_load_configuration_parses_contents():
  7. builtins = flexmock(sys.modules['builtins'])
  8. config_file = io.StringIO('key: value')
  9. config_file.name = 'config.yaml'
  10. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  11. assert module.load_configuration('config.yaml') == {'key': 'value'}
  12. def test_load_configuration_replaces_constants():
  13. builtins = flexmock(sys.modules['builtins'])
  14. config_file = io.StringIO(
  15. '''
  16. constants:
  17. key: value
  18. key: {key}
  19. '''
  20. )
  21. config_file.name = 'config.yaml'
  22. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  23. assert module.load_configuration('config.yaml') == {'key': 'value'}
  24. def test_load_configuration_replaces_complex_constants():
  25. builtins = flexmock(sys.modules['builtins'])
  26. config_file = io.StringIO(
  27. '''
  28. constants:
  29. key:
  30. subkey: value
  31. key: {key}
  32. '''
  33. )
  34. config_file.name = 'config.yaml'
  35. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  36. assert module.load_configuration('config.yaml') == {'key': {'subkey': 'value'}}
  37. def test_load_configuration_inlines_include_relative_to_current_directory():
  38. builtins = flexmock(sys.modules['builtins'])
  39. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  40. flexmock(module.os.path).should_receive('isabs').and_return(False)
  41. flexmock(module.os.path).should_receive('exists').and_return(True)
  42. include_file = io.StringIO('value')
  43. include_file.name = 'include.yaml'
  44. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  45. config_file = io.StringIO('key: !include include.yaml')
  46. config_file.name = 'config.yaml'
  47. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  48. assert module.load_configuration('config.yaml') == {'key': 'value'}
  49. def test_load_configuration_inlines_include_relative_to_config_parent_directory():
  50. builtins = flexmock(sys.modules['builtins'])
  51. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  52. flexmock(module.os.path).should_receive('isabs').with_args('/etc').and_return(True)
  53. flexmock(module.os.path).should_receive('isabs').with_args('/etc/config.yaml').and_return(True)
  54. flexmock(module.os.path).should_receive('isabs').with_args('include.yaml').and_return(False)
  55. flexmock(module.os.path).should_receive('exists').with_args('/tmp/include.yaml').and_return(
  56. False
  57. )
  58. flexmock(module.os.path).should_receive('exists').with_args('/etc/include.yaml').and_return(
  59. True
  60. )
  61. include_file = io.StringIO('value')
  62. include_file.name = 'include.yaml'
  63. builtins.should_receive('open').with_args('/etc/include.yaml').and_return(include_file)
  64. config_file = io.StringIO('key: !include include.yaml')
  65. config_file.name = '/etc/config.yaml'
  66. builtins.should_receive('open').with_args('/etc/config.yaml').and_return(config_file)
  67. assert module.load_configuration('/etc/config.yaml') == {'key': 'value'}
  68. def test_load_configuration_raises_if_relative_include_does_not_exist():
  69. builtins = flexmock(sys.modules['builtins'])
  70. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  71. flexmock(module.os.path).should_receive('isabs').with_args('/etc').and_return(True)
  72. flexmock(module.os.path).should_receive('isabs').with_args('/etc/config.yaml').and_return(True)
  73. flexmock(module.os.path).should_receive('isabs').with_args('include.yaml').and_return(False)
  74. flexmock(module.os.path).should_receive('exists').and_return(False)
  75. config_file = io.StringIO('key: !include include.yaml')
  76. config_file.name = '/etc/config.yaml'
  77. builtins.should_receive('open').with_args('/etc/config.yaml').and_return(config_file)
  78. with pytest.raises(FileNotFoundError):
  79. module.load_configuration('/etc/config.yaml')
  80. def test_load_configuration_inlines_absolute_include():
  81. builtins = flexmock(sys.modules['builtins'])
  82. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  83. flexmock(module.os.path).should_receive('isabs').and_return(True)
  84. flexmock(module.os.path).should_receive('exists').never()
  85. include_file = io.StringIO('value')
  86. include_file.name = '/root/include.yaml'
  87. builtins.should_receive('open').with_args('/root/include.yaml').and_return(include_file)
  88. config_file = io.StringIO('key: !include /root/include.yaml')
  89. config_file.name = 'config.yaml'
  90. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  91. assert module.load_configuration('config.yaml') == {'key': 'value'}
  92. def test_load_configuration_raises_if_absolute_include_does_not_exist():
  93. builtins = flexmock(sys.modules['builtins'])
  94. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  95. flexmock(module.os.path).should_receive('isabs').and_return(True)
  96. builtins.should_receive('open').with_args('/root/include.yaml').and_raise(FileNotFoundError)
  97. config_file = io.StringIO('key: !include /root/include.yaml')
  98. config_file.name = 'config.yaml'
  99. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  100. with pytest.raises(FileNotFoundError):
  101. assert module.load_configuration('config.yaml')
  102. def test_load_configuration_merges_include():
  103. builtins = flexmock(sys.modules['builtins'])
  104. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  105. flexmock(module.os.path).should_receive('isabs').and_return(False)
  106. flexmock(module.os.path).should_receive('exists').and_return(True)
  107. include_file = io.StringIO(
  108. '''
  109. foo: bar
  110. baz: quux
  111. '''
  112. )
  113. include_file.name = 'include.yaml'
  114. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  115. config_file = io.StringIO(
  116. '''
  117. foo: override
  118. <<: !include include.yaml
  119. '''
  120. )
  121. config_file.name = 'config.yaml'
  122. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  123. assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'}
  124. def test_load_configuration_with_retain_tag_merges_include_but_keeps_local_values():
  125. builtins = flexmock(sys.modules['builtins'])
  126. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  127. flexmock(module.os.path).should_receive('isabs').and_return(False)
  128. flexmock(module.os.path).should_receive('exists').and_return(True)
  129. include_file = io.StringIO(
  130. '''
  131. stuff:
  132. foo: bar
  133. baz: quux
  134. other:
  135. a: b
  136. c: d
  137. '''
  138. )
  139. include_file.name = 'include.yaml'
  140. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  141. config_file = io.StringIO(
  142. '''
  143. stuff: !retain
  144. foo: override
  145. other:
  146. a: override
  147. <<: !include include.yaml
  148. '''
  149. )
  150. config_file.name = 'config.yaml'
  151. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  152. assert module.load_configuration('config.yaml') == {
  153. 'stuff': {'foo': 'override'},
  154. 'other': {'a': 'override', 'c': 'd'},
  155. }
  156. def test_load_configuration_with_retain_tag_but_without_merge_include_raises():
  157. builtins = flexmock(sys.modules['builtins'])
  158. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  159. flexmock(module.os.path).should_receive('isabs').and_return(False)
  160. flexmock(module.os.path).should_receive('exists').and_return(True)
  161. include_file = io.StringIO(
  162. '''
  163. stuff: !retain
  164. foo: bar
  165. baz: quux
  166. '''
  167. )
  168. include_file.name = 'include.yaml'
  169. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  170. config_file = io.StringIO(
  171. '''
  172. stuff:
  173. foo: override
  174. <<: !include include.yaml
  175. '''
  176. )
  177. config_file.name = 'config.yaml'
  178. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  179. with pytest.raises(ValueError):
  180. module.load_configuration('config.yaml')
  181. def test_load_configuration_with_retain_tag_on_scalar_raises():
  182. builtins = flexmock(sys.modules['builtins'])
  183. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  184. flexmock(module.os.path).should_receive('isabs').and_return(False)
  185. flexmock(module.os.path).should_receive('exists').and_return(True)
  186. include_file = io.StringIO(
  187. '''
  188. stuff:
  189. foo: bar
  190. baz: quux
  191. '''
  192. )
  193. include_file.name = 'include.yaml'
  194. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  195. config_file = io.StringIO(
  196. '''
  197. stuff:
  198. foo: !retain override
  199. <<: !include include.yaml
  200. '''
  201. )
  202. config_file.name = 'config.yaml'
  203. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  204. with pytest.raises(ValueError):
  205. module.load_configuration('config.yaml')
  206. def test_load_configuration_with_omit_tag_merges_include_and_omits_requested_values():
  207. builtins = flexmock(sys.modules['builtins'])
  208. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  209. flexmock(module.os.path).should_receive('isabs').and_return(False)
  210. flexmock(module.os.path).should_receive('exists').and_return(True)
  211. include_file = io.StringIO(
  212. '''
  213. stuff:
  214. - a
  215. - b
  216. - c
  217. '''
  218. )
  219. include_file.name = 'include.yaml'
  220. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  221. config_file = io.StringIO(
  222. '''
  223. stuff:
  224. - x
  225. - !omit b
  226. - y
  227. <<: !include include.yaml
  228. '''
  229. )
  230. config_file.name = 'config.yaml'
  231. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  232. assert module.load_configuration('config.yaml') == {'stuff': ['a', 'c', 'x', 'y']}
  233. def test_load_configuration_with_omit_tag_on_unknown_value_merges_include_and_does_not_raise():
  234. builtins = flexmock(sys.modules['builtins'])
  235. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  236. flexmock(module.os.path).should_receive('isabs').and_return(False)
  237. flexmock(module.os.path).should_receive('exists').and_return(True)
  238. include_file = io.StringIO(
  239. '''
  240. stuff:
  241. - a
  242. - b
  243. - c
  244. '''
  245. )
  246. include_file.name = 'include.yaml'
  247. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  248. config_file = io.StringIO(
  249. '''
  250. stuff:
  251. - x
  252. - !omit q
  253. - y
  254. <<: !include include.yaml
  255. '''
  256. )
  257. config_file.name = 'config.yaml'
  258. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  259. assert module.load_configuration('config.yaml') == {'stuff': ['a', 'b', 'c', 'x', 'y']}
  260. def test_load_configuration_with_omit_tag_on_non_list_item_raises():
  261. builtins = flexmock(sys.modules['builtins'])
  262. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  263. flexmock(module.os.path).should_receive('isabs').and_return(False)
  264. flexmock(module.os.path).should_receive('exists').and_return(True)
  265. include_file = io.StringIO(
  266. '''
  267. stuff:
  268. - a
  269. - b
  270. - c
  271. '''
  272. )
  273. include_file.name = 'include.yaml'
  274. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  275. config_file = io.StringIO(
  276. '''
  277. stuff: !omit
  278. - x
  279. - y
  280. <<: !include include.yaml
  281. '''
  282. )
  283. config_file.name = 'config.yaml'
  284. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  285. with pytest.raises(ValueError):
  286. module.load_configuration('config.yaml')
  287. def test_load_configuration_with_omit_tag_on_non_scalar_list_item_raises():
  288. builtins = flexmock(sys.modules['builtins'])
  289. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  290. flexmock(module.os.path).should_receive('isabs').and_return(False)
  291. flexmock(module.os.path).should_receive('exists').and_return(True)
  292. include_file = io.StringIO(
  293. '''
  294. stuff:
  295. - foo: bar
  296. baz: quux
  297. '''
  298. )
  299. include_file.name = 'include.yaml'
  300. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  301. config_file = io.StringIO(
  302. '''
  303. stuff:
  304. - !omit foo: bar
  305. baz: quux
  306. <<: !include include.yaml
  307. '''
  308. )
  309. config_file.name = 'config.yaml'
  310. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  311. with pytest.raises(ValueError):
  312. module.load_configuration('config.yaml')
  313. def test_load_configuration_with_omit_tag_but_without_merge_raises():
  314. builtins = flexmock(sys.modules['builtins'])
  315. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  316. flexmock(module.os.path).should_receive('isabs').and_return(False)
  317. flexmock(module.os.path).should_receive('exists').and_return(True)
  318. include_file = io.StringIO(
  319. '''
  320. stuff:
  321. - a
  322. - !omit b
  323. - c
  324. '''
  325. )
  326. include_file.name = 'include.yaml'
  327. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  328. config_file = io.StringIO(
  329. '''
  330. stuff:
  331. - x
  332. - y
  333. <<: !include include.yaml
  334. '''
  335. )
  336. config_file.name = 'config.yaml'
  337. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  338. with pytest.raises(ValueError):
  339. module.load_configuration('config.yaml')
  340. def test_load_configuration_does_not_merge_include_list():
  341. builtins = flexmock(sys.modules['builtins'])
  342. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  343. flexmock(module.os.path).should_receive('isabs').and_return(False)
  344. flexmock(module.os.path).should_receive('exists').and_return(True)
  345. include_file = io.StringIO(
  346. '''
  347. - one
  348. - two
  349. '''
  350. )
  351. include_file.name = 'include.yaml'
  352. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  353. config_file = io.StringIO(
  354. '''
  355. foo: bar
  356. repositories:
  357. <<: !include include.yaml
  358. '''
  359. )
  360. config_file.name = 'config.yaml'
  361. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  362. with pytest.raises(module.ruamel.yaml.error.YAMLError):
  363. assert module.load_configuration('config.yaml')
  364. @pytest.mark.parametrize(
  365. 'node_class',
  366. (
  367. module.ruamel.yaml.nodes.MappingNode,
  368. module.ruamel.yaml.nodes.SequenceNode,
  369. module.ruamel.yaml.nodes.ScalarNode,
  370. ),
  371. )
  372. def test_raise_retain_node_error_raises(node_class):
  373. with pytest.raises(ValueError):
  374. module.raise_retain_node_error(
  375. loader=flexmock(), node=node_class(tag=flexmock(), value=flexmock())
  376. )
  377. def test_raise_omit_node_error_raises():
  378. with pytest.raises(ValueError):
  379. module.raise_omit_node_error(loader=flexmock(), node=flexmock())
  380. def test_filter_omitted_nodes():
  381. nodes = [
  382. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='a'),
  383. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='b'),
  384. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
  385. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='a'),
  386. module.ruamel.yaml.nodes.ScalarNode(tag='!omit', value='b'),
  387. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
  388. ]
  389. result = module.filter_omitted_nodes(nodes)
  390. assert [item.value for item in result] == ['a', 'c', 'a', 'c']
  391. def test_deep_merge_nodes_replaces_colliding_scalar_values():
  392. node_values = [
  393. (
  394. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  395. module.ruamel.yaml.nodes.MappingNode(
  396. tag='tag:yaml.org,2002:map',
  397. value=[
  398. (
  399. module.ruamel.yaml.nodes.ScalarNode(
  400. tag='tag:yaml.org,2002:str', value='keep_hourly'
  401. ),
  402. module.ruamel.yaml.nodes.ScalarNode(
  403. tag='tag:yaml.org,2002:int', value='24'
  404. ),
  405. ),
  406. (
  407. module.ruamel.yaml.nodes.ScalarNode(
  408. tag='tag:yaml.org,2002:str', value='keep_daily'
  409. ),
  410. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
  411. ),
  412. ],
  413. ),
  414. ),
  415. (
  416. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  417. module.ruamel.yaml.nodes.MappingNode(
  418. tag='tag:yaml.org,2002:map',
  419. value=[
  420. (
  421. module.ruamel.yaml.nodes.ScalarNode(
  422. tag='tag:yaml.org,2002:str', value='keep_daily'
  423. ),
  424. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
  425. ),
  426. ],
  427. ),
  428. ),
  429. ]
  430. result = module.deep_merge_nodes(node_values)
  431. assert len(result) == 1
  432. (section_key, section_value) = result[0]
  433. assert section_key.value == 'retention'
  434. options = section_value.value
  435. assert len(options) == 2
  436. assert options[0][0].value == 'keep_hourly'
  437. assert options[0][1].value == '24'
  438. assert options[1][0].value == 'keep_daily'
  439. assert options[1][1].value == '5'
  440. def test_deep_merge_nodes_keeps_non_colliding_scalar_values():
  441. node_values = [
  442. (
  443. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  444. module.ruamel.yaml.nodes.MappingNode(
  445. tag='tag:yaml.org,2002:map',
  446. value=[
  447. (
  448. module.ruamel.yaml.nodes.ScalarNode(
  449. tag='tag:yaml.org,2002:str', value='keep_hourly'
  450. ),
  451. module.ruamel.yaml.nodes.ScalarNode(
  452. tag='tag:yaml.org,2002:int', value='24'
  453. ),
  454. ),
  455. (
  456. module.ruamel.yaml.nodes.ScalarNode(
  457. tag='tag:yaml.org,2002:str', value='keep_daily'
  458. ),
  459. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
  460. ),
  461. ],
  462. ),
  463. ),
  464. (
  465. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  466. module.ruamel.yaml.nodes.MappingNode(
  467. tag='tag:yaml.org,2002:map',
  468. value=[
  469. (
  470. module.ruamel.yaml.nodes.ScalarNode(
  471. tag='tag:yaml.org,2002:str', value='keep_minutely'
  472. ),
  473. module.ruamel.yaml.nodes.ScalarNode(
  474. tag='tag:yaml.org,2002:int', value='10'
  475. ),
  476. ),
  477. ],
  478. ),
  479. ),
  480. ]
  481. result = module.deep_merge_nodes(node_values)
  482. assert len(result) == 1
  483. (section_key, section_value) = result[0]
  484. assert section_key.value == 'retention'
  485. options = section_value.value
  486. assert len(options) == 3
  487. assert options[0][0].value == 'keep_hourly'
  488. assert options[0][1].value == '24'
  489. assert options[1][0].value == 'keep_daily'
  490. assert options[1][1].value == '7'
  491. assert options[2][0].value == 'keep_minutely'
  492. assert options[2][1].value == '10'
  493. def test_deep_merge_nodes_keeps_deeply_nested_values():
  494. node_values = [
  495. (
  496. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'),
  497. module.ruamel.yaml.nodes.MappingNode(
  498. tag='tag:yaml.org,2002:map',
  499. value=[
  500. (
  501. module.ruamel.yaml.nodes.ScalarNode(
  502. tag='tag:yaml.org,2002:str', value='lock_wait'
  503. ),
  504. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
  505. ),
  506. (
  507. module.ruamel.yaml.nodes.ScalarNode(
  508. tag='tag:yaml.org,2002:str', value='extra_borg_options'
  509. ),
  510. module.ruamel.yaml.nodes.MappingNode(
  511. tag='tag:yaml.org,2002:map',
  512. value=[
  513. (
  514. module.ruamel.yaml.nodes.ScalarNode(
  515. tag='tag:yaml.org,2002:str', value='init'
  516. ),
  517. module.ruamel.yaml.nodes.ScalarNode(
  518. tag='tag:yaml.org,2002:str', value='--init-option'
  519. ),
  520. ),
  521. ],
  522. ),
  523. ),
  524. ],
  525. ),
  526. ),
  527. (
  528. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'),
  529. module.ruamel.yaml.nodes.MappingNode(
  530. tag='tag:yaml.org,2002:map',
  531. value=[
  532. (
  533. module.ruamel.yaml.nodes.ScalarNode(
  534. tag='tag:yaml.org,2002:str', value='extra_borg_options'
  535. ),
  536. module.ruamel.yaml.nodes.MappingNode(
  537. tag='tag:yaml.org,2002:map',
  538. value=[
  539. (
  540. module.ruamel.yaml.nodes.ScalarNode(
  541. tag='tag:yaml.org,2002:str', value='prune'
  542. ),
  543. module.ruamel.yaml.nodes.ScalarNode(
  544. tag='tag:yaml.org,2002:str', value='--prune-option'
  545. ),
  546. ),
  547. ],
  548. ),
  549. ),
  550. ],
  551. ),
  552. ),
  553. ]
  554. result = module.deep_merge_nodes(node_values)
  555. assert len(result) == 1
  556. (section_key, section_value) = result[0]
  557. assert section_key.value == 'storage'
  558. options = section_value.value
  559. assert len(options) == 2
  560. assert options[0][0].value == 'lock_wait'
  561. assert options[0][1].value == '5'
  562. assert options[1][0].value == 'extra_borg_options'
  563. nested_options = options[1][1].value
  564. assert len(nested_options) == 2
  565. assert nested_options[0][0].value == 'init'
  566. assert nested_options[0][1].value == '--init-option'
  567. assert nested_options[1][0].value == 'prune'
  568. assert nested_options[1][1].value == '--prune-option'
  569. def test_deep_merge_nodes_appends_colliding_sequence_values():
  570. node_values = [
  571. (
  572. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  573. module.ruamel.yaml.nodes.MappingNode(
  574. tag='tag:yaml.org,2002:map',
  575. value=[
  576. (
  577. module.ruamel.yaml.nodes.ScalarNode(
  578. tag='tag:yaml.org,2002:str', value='before_backup'
  579. ),
  580. module.ruamel.yaml.nodes.SequenceNode(
  581. tag='tag:yaml.org,2002:seq',
  582. value=[
  583. module.ruamel.yaml.ScalarNode(
  584. tag='tag:yaml.org,2002:str', value='echo 1'
  585. ),
  586. module.ruamel.yaml.ScalarNode(
  587. tag='tag:yaml.org,2002:str', value='echo 2'
  588. ),
  589. ],
  590. ),
  591. ),
  592. ],
  593. ),
  594. ),
  595. (
  596. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  597. module.ruamel.yaml.nodes.MappingNode(
  598. tag='tag:yaml.org,2002:map',
  599. value=[
  600. (
  601. module.ruamel.yaml.nodes.ScalarNode(
  602. tag='tag:yaml.org,2002:str', value='before_backup'
  603. ),
  604. module.ruamel.yaml.nodes.SequenceNode(
  605. tag='tag:yaml.org,2002:seq',
  606. value=[
  607. module.ruamel.yaml.ScalarNode(
  608. tag='tag:yaml.org,2002:str', value='echo 3'
  609. ),
  610. module.ruamel.yaml.ScalarNode(
  611. tag='tag:yaml.org,2002:str', value='echo 4'
  612. ),
  613. ],
  614. ),
  615. ),
  616. ],
  617. ),
  618. ),
  619. ]
  620. result = module.deep_merge_nodes(node_values)
  621. assert len(result) == 1
  622. (section_key, section_value) = result[0]
  623. assert section_key.value == 'hooks'
  624. options = section_value.value
  625. assert len(options) == 1
  626. assert options[0][0].value == 'before_backup'
  627. assert [item.value for item in options[0][1].value] == ['echo 1', 'echo 2', 'echo 3', 'echo 4']
  628. def test_deep_merge_nodes_only_keeps_mapping_values_tagged_with_retain():
  629. node_values = [
  630. (
  631. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  632. module.ruamel.yaml.nodes.MappingNode(
  633. tag='tag:yaml.org,2002:map',
  634. value=[
  635. (
  636. module.ruamel.yaml.nodes.ScalarNode(
  637. tag='tag:yaml.org,2002:str', value='keep_hourly'
  638. ),
  639. module.ruamel.yaml.nodes.ScalarNode(
  640. tag='tag:yaml.org,2002:int', value='24'
  641. ),
  642. ),
  643. (
  644. module.ruamel.yaml.nodes.ScalarNode(
  645. tag='tag:yaml.org,2002:str', value='keep_daily'
  646. ),
  647. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
  648. ),
  649. ],
  650. ),
  651. ),
  652. (
  653. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  654. module.ruamel.yaml.nodes.MappingNode(
  655. tag='!retain',
  656. value=[
  657. (
  658. module.ruamel.yaml.nodes.ScalarNode(
  659. tag='tag:yaml.org,2002:str', value='keep_daily'
  660. ),
  661. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
  662. ),
  663. ],
  664. ),
  665. ),
  666. ]
  667. result = module.deep_merge_nodes(node_values)
  668. assert len(result) == 1
  669. (section_key, section_value) = result[0]
  670. assert section_key.value == 'retention'
  671. assert section_value.tag == 'tag:yaml.org,2002:map'
  672. options = section_value.value
  673. assert len(options) == 1
  674. assert options[0][0].value == 'keep_daily'
  675. assert options[0][1].value == '5'
  676. def test_deep_merge_nodes_only_keeps_sequence_values_tagged_with_retain():
  677. node_values = [
  678. (
  679. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  680. module.ruamel.yaml.nodes.MappingNode(
  681. tag='tag:yaml.org,2002:map',
  682. value=[
  683. (
  684. module.ruamel.yaml.nodes.ScalarNode(
  685. tag='tag:yaml.org,2002:str', value='before_backup'
  686. ),
  687. module.ruamel.yaml.nodes.SequenceNode(
  688. tag='tag:yaml.org,2002:seq',
  689. value=[
  690. module.ruamel.yaml.ScalarNode(
  691. tag='tag:yaml.org,2002:str', value='echo 1'
  692. ),
  693. module.ruamel.yaml.ScalarNode(
  694. tag='tag:yaml.org,2002:str', value='echo 2'
  695. ),
  696. ],
  697. ),
  698. ),
  699. ],
  700. ),
  701. ),
  702. (
  703. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  704. module.ruamel.yaml.nodes.MappingNode(
  705. tag='tag:yaml.org,2002:map',
  706. value=[
  707. (
  708. module.ruamel.yaml.nodes.ScalarNode(
  709. tag='tag:yaml.org,2002:str', value='before_backup'
  710. ),
  711. module.ruamel.yaml.nodes.SequenceNode(
  712. tag='!retain',
  713. value=[
  714. module.ruamel.yaml.ScalarNode(
  715. tag='tag:yaml.org,2002:str', value='echo 3'
  716. ),
  717. module.ruamel.yaml.ScalarNode(
  718. tag='tag:yaml.org,2002:str', value='echo 4'
  719. ),
  720. ],
  721. ),
  722. ),
  723. ],
  724. ),
  725. ),
  726. ]
  727. result = module.deep_merge_nodes(node_values)
  728. assert len(result) == 1
  729. (section_key, section_value) = result[0]
  730. assert section_key.value == 'hooks'
  731. options = section_value.value
  732. assert len(options) == 1
  733. assert options[0][0].value == 'before_backup'
  734. assert options[0][1].tag == 'tag:yaml.org,2002:seq'
  735. assert [item.value for item in options[0][1].value] == ['echo 3', 'echo 4']
  736. def test_deep_merge_nodes_skips_sequence_values_tagged_with_omit():
  737. node_values = [
  738. (
  739. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  740. module.ruamel.yaml.nodes.MappingNode(
  741. tag='tag:yaml.org,2002:map',
  742. value=[
  743. (
  744. module.ruamel.yaml.nodes.ScalarNode(
  745. tag='tag:yaml.org,2002:str', value='before_backup'
  746. ),
  747. module.ruamel.yaml.nodes.SequenceNode(
  748. tag='tag:yaml.org,2002:seq',
  749. value=[
  750. module.ruamel.yaml.ScalarNode(
  751. tag='tag:yaml.org,2002:str', value='echo 1'
  752. ),
  753. module.ruamel.yaml.ScalarNode(
  754. tag='tag:yaml.org,2002:str', value='echo 2'
  755. ),
  756. ],
  757. ),
  758. ),
  759. ],
  760. ),
  761. ),
  762. (
  763. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  764. module.ruamel.yaml.nodes.MappingNode(
  765. tag='tag:yaml.org,2002:map',
  766. value=[
  767. (
  768. module.ruamel.yaml.nodes.ScalarNode(
  769. tag='tag:yaml.org,2002:str', value='before_backup'
  770. ),
  771. module.ruamel.yaml.nodes.SequenceNode(
  772. tag='tag:yaml.org,2002:seq',
  773. value=[
  774. module.ruamel.yaml.ScalarNode(tag='!omit', value='echo 2'),
  775. module.ruamel.yaml.ScalarNode(
  776. tag='tag:yaml.org,2002:str', value='echo 3'
  777. ),
  778. ],
  779. ),
  780. ),
  781. ],
  782. ),
  783. ),
  784. ]
  785. result = module.deep_merge_nodes(node_values)
  786. assert len(result) == 1
  787. (section_key, section_value) = result[0]
  788. assert section_key.value == 'hooks'
  789. options = section_value.value
  790. assert len(options) == 1
  791. assert options[0][0].value == 'before_backup'
  792. assert [item.value for item in options[0][1].value] == ['echo 1', 'echo 3']