test_load.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100
  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_with_only_integer_value_does_not_raise():
  13. builtins = flexmock(sys.modules['builtins'])
  14. config_file = io.StringIO('33')
  15. config_file.name = 'config.yaml'
  16. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  17. assert module.load_configuration('config.yaml') == 33
  18. def test_load_configuration_inlines_include_relative_to_current_directory():
  19. builtins = flexmock(sys.modules['builtins'])
  20. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  21. flexmock(module.os.path).should_receive('isabs').and_return(False)
  22. flexmock(module.os.path).should_receive('exists').and_return(True)
  23. include_file = io.StringIO('value')
  24. include_file.name = 'include.yaml'
  25. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  26. config_file = io.StringIO('key: !include include.yaml')
  27. config_file.name = 'config.yaml'
  28. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  29. assert module.load_configuration('config.yaml') == {'key': 'value'}
  30. def test_load_configuration_inlines_include_relative_to_config_parent_directory():
  31. builtins = flexmock(sys.modules['builtins'])
  32. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  33. flexmock(module.os.path).should_receive('isabs').with_args('/etc').and_return(True)
  34. flexmock(module.os.path).should_receive('isabs').with_args('/etc/config.yaml').and_return(True)
  35. flexmock(module.os.path).should_receive('isabs').with_args('include.yaml').and_return(False)
  36. flexmock(module.os.path).should_receive('exists').with_args('/tmp/include.yaml').and_return(
  37. False
  38. )
  39. flexmock(module.os.path).should_receive('exists').with_args('/etc/include.yaml').and_return(
  40. True
  41. )
  42. include_file = io.StringIO('value')
  43. include_file.name = 'include.yaml'
  44. builtins.should_receive('open').with_args('/etc/include.yaml').and_return(include_file)
  45. config_file = io.StringIO('key: !include include.yaml')
  46. config_file.name = '/etc/config.yaml'
  47. builtins.should_receive('open').with_args('/etc/config.yaml').and_return(config_file)
  48. assert module.load_configuration('/etc/config.yaml') == {'key': 'value'}
  49. def test_load_configuration_raises_if_relative_include_does_not_exist():
  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').and_return(False)
  56. config_file = io.StringIO('key: !include include.yaml')
  57. config_file.name = '/etc/config.yaml'
  58. builtins.should_receive('open').with_args('/etc/config.yaml').and_return(config_file)
  59. with pytest.raises(FileNotFoundError):
  60. module.load_configuration('/etc/config.yaml')
  61. def test_load_configuration_inlines_absolute_include():
  62. builtins = flexmock(sys.modules['builtins'])
  63. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  64. flexmock(module.os.path).should_receive('isabs').and_return(True)
  65. flexmock(module.os.path).should_receive('exists').never()
  66. include_file = io.StringIO('value')
  67. include_file.name = '/root/include.yaml'
  68. builtins.should_receive('open').with_args('/root/include.yaml').and_return(include_file)
  69. config_file = io.StringIO('key: !include /root/include.yaml')
  70. config_file.name = 'config.yaml'
  71. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  72. assert module.load_configuration('config.yaml') == {'key': 'value'}
  73. def test_load_configuration_raises_if_absolute_include_does_not_exist():
  74. builtins = flexmock(sys.modules['builtins'])
  75. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  76. flexmock(module.os.path).should_receive('isabs').and_return(True)
  77. builtins.should_receive('open').with_args('/root/include.yaml').and_raise(FileNotFoundError)
  78. config_file = io.StringIO('key: !include /root/include.yaml')
  79. config_file.name = 'config.yaml'
  80. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  81. with pytest.raises(FileNotFoundError):
  82. assert module.load_configuration('config.yaml')
  83. def test_load_configuration_inlines_multiple_file_include_as_list():
  84. builtins = flexmock(sys.modules['builtins'])
  85. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  86. flexmock(module.os.path).should_receive('isabs').and_return(True)
  87. flexmock(module.os.path).should_receive('exists').never()
  88. include1_file = io.StringIO('value1')
  89. include1_file.name = '/root/include1.yaml'
  90. builtins.should_receive('open').with_args('/root/include1.yaml').and_return(include1_file)
  91. include2_file = io.StringIO('value2')
  92. include2_file.name = '/root/include2.yaml'
  93. builtins.should_receive('open').with_args('/root/include2.yaml').and_return(include2_file)
  94. config_file = io.StringIO('key: !include [/root/include1.yaml, /root/include2.yaml]')
  95. config_file.name = 'config.yaml'
  96. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  97. assert module.load_configuration('config.yaml') == {'key': ['value2', 'value1']}
  98. def test_load_configuration_include_with_unsupported_filename_type_raises():
  99. builtins = flexmock(sys.modules['builtins'])
  100. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  101. flexmock(module.os.path).should_receive('isabs').and_return(True)
  102. flexmock(module.os.path).should_receive('exists').never()
  103. config_file = io.StringIO('key: !include {path: /root/include.yaml}')
  104. config_file.name = 'config.yaml'
  105. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  106. with pytest.raises(ValueError):
  107. module.load_configuration('config.yaml')
  108. def test_load_configuration_merges_include():
  109. builtins = flexmock(sys.modules['builtins'])
  110. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  111. flexmock(module.os.path).should_receive('isabs').and_return(False)
  112. flexmock(module.os.path).should_receive('exists').and_return(True)
  113. include_file = io.StringIO(
  114. '''
  115. foo: bar
  116. baz: quux
  117. '''
  118. )
  119. include_file.name = 'include.yaml'
  120. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  121. config_file = io.StringIO(
  122. '''
  123. foo: override
  124. <<: !include include.yaml
  125. '''
  126. )
  127. config_file.name = 'config.yaml'
  128. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  129. assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'}
  130. def test_load_configuration_merges_multiple_file_include():
  131. builtins = flexmock(sys.modules['builtins'])
  132. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  133. flexmock(module.os.path).should_receive('isabs').and_return(False)
  134. flexmock(module.os.path).should_receive('exists').and_return(True)
  135. include1_file = io.StringIO(
  136. '''
  137. foo: bar
  138. baz: quux
  139. original: yes
  140. '''
  141. )
  142. include1_file.name = 'include1.yaml'
  143. builtins.should_receive('open').with_args('/tmp/include1.yaml').and_return(include1_file)
  144. include2_file = io.StringIO(
  145. '''
  146. baz: second
  147. '''
  148. )
  149. include2_file.name = 'include2.yaml'
  150. builtins.should_receive('open').with_args('/tmp/include2.yaml').and_return(include2_file)
  151. config_file = io.StringIO(
  152. '''
  153. foo: override
  154. <<: !include [include1.yaml, include2.yaml]
  155. '''
  156. )
  157. config_file.name = 'config.yaml'
  158. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  159. assert module.load_configuration('config.yaml') == {
  160. 'foo': 'override',
  161. 'baz': 'second',
  162. 'original': 'yes',
  163. }
  164. def test_load_configuration_with_retain_tag_merges_include_but_keeps_local_values():
  165. builtins = flexmock(sys.modules['builtins'])
  166. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  167. flexmock(module.os.path).should_receive('isabs').and_return(False)
  168. flexmock(module.os.path).should_receive('exists').and_return(True)
  169. include_file = io.StringIO(
  170. '''
  171. stuff:
  172. foo: bar
  173. baz: quux
  174. other:
  175. a: b
  176. c: d
  177. '''
  178. )
  179. include_file.name = 'include.yaml'
  180. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  181. config_file = io.StringIO(
  182. '''
  183. stuff: !retain
  184. foo: override
  185. other:
  186. a: override
  187. <<: !include include.yaml
  188. '''
  189. )
  190. config_file.name = 'config.yaml'
  191. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  192. assert module.load_configuration('config.yaml') == {
  193. 'stuff': {'foo': 'override'},
  194. 'other': {'a': 'override', 'c': 'd'},
  195. }
  196. def test_load_configuration_with_retain_tag_but_without_merge_include_raises():
  197. builtins = flexmock(sys.modules['builtins'])
  198. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  199. flexmock(module.os.path).should_receive('isabs').and_return(False)
  200. flexmock(module.os.path).should_receive('exists').and_return(True)
  201. include_file = io.StringIO(
  202. '''
  203. stuff: !retain
  204. foo: bar
  205. baz: quux
  206. '''
  207. )
  208. include_file.name = 'include.yaml'
  209. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  210. config_file = io.StringIO(
  211. '''
  212. stuff:
  213. foo: override
  214. <<: !include include.yaml
  215. '''
  216. )
  217. config_file.name = 'config.yaml'
  218. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  219. with pytest.raises(ValueError):
  220. module.load_configuration('config.yaml')
  221. def test_load_configuration_with_retain_tag_on_scalar_raises():
  222. builtins = flexmock(sys.modules['builtins'])
  223. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  224. flexmock(module.os.path).should_receive('isabs').and_return(False)
  225. flexmock(module.os.path).should_receive('exists').and_return(True)
  226. include_file = io.StringIO(
  227. '''
  228. stuff:
  229. foo: bar
  230. baz: quux
  231. '''
  232. )
  233. include_file.name = 'include.yaml'
  234. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  235. config_file = io.StringIO(
  236. '''
  237. stuff:
  238. foo: !retain override
  239. <<: !include include.yaml
  240. '''
  241. )
  242. config_file.name = 'config.yaml'
  243. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  244. with pytest.raises(ValueError):
  245. module.load_configuration('config.yaml')
  246. def test_load_configuration_with_omit_tag_merges_include_and_omits_requested_values():
  247. builtins = flexmock(sys.modules['builtins'])
  248. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  249. flexmock(module.os.path).should_receive('isabs').and_return(False)
  250. flexmock(module.os.path).should_receive('exists').and_return(True)
  251. include_file = io.StringIO(
  252. '''
  253. stuff:
  254. - a
  255. - b
  256. - c
  257. '''
  258. )
  259. include_file.name = 'include.yaml'
  260. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  261. config_file = io.StringIO(
  262. '''
  263. stuff:
  264. - x
  265. - !omit b
  266. - y
  267. <<: !include include.yaml
  268. '''
  269. )
  270. config_file.name = 'config.yaml'
  271. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  272. assert module.load_configuration('config.yaml') == {'stuff': ['a', 'c', 'x', 'y']}
  273. def test_load_configuration_with_omit_tag_on_unknown_value_merges_include_and_does_not_raise():
  274. builtins = flexmock(sys.modules['builtins'])
  275. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  276. flexmock(module.os.path).should_receive('isabs').and_return(False)
  277. flexmock(module.os.path).should_receive('exists').and_return(True)
  278. include_file = io.StringIO(
  279. '''
  280. stuff:
  281. - a
  282. - b
  283. - c
  284. '''
  285. )
  286. include_file.name = 'include.yaml'
  287. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  288. config_file = io.StringIO(
  289. '''
  290. stuff:
  291. - x
  292. - !omit q
  293. - y
  294. <<: !include include.yaml
  295. '''
  296. )
  297. config_file.name = 'config.yaml'
  298. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  299. assert module.load_configuration('config.yaml') == {'stuff': ['a', 'b', 'c', 'x', 'y']}
  300. def test_load_configuration_with_omit_tag_on_non_list_item_raises():
  301. builtins = flexmock(sys.modules['builtins'])
  302. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  303. flexmock(module.os.path).should_receive('isabs').and_return(False)
  304. flexmock(module.os.path).should_receive('exists').and_return(True)
  305. include_file = io.StringIO(
  306. '''
  307. stuff:
  308. - a
  309. - b
  310. - c
  311. '''
  312. )
  313. include_file.name = 'include.yaml'
  314. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  315. config_file = io.StringIO(
  316. '''
  317. stuff: !omit
  318. - x
  319. - y
  320. <<: !include include.yaml
  321. '''
  322. )
  323. config_file.name = 'config.yaml'
  324. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  325. with pytest.raises(ValueError):
  326. module.load_configuration('config.yaml')
  327. def test_load_configuration_with_omit_tag_on_non_scalar_list_item_raises():
  328. builtins = flexmock(sys.modules['builtins'])
  329. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  330. flexmock(module.os.path).should_receive('isabs').and_return(False)
  331. flexmock(module.os.path).should_receive('exists').and_return(True)
  332. include_file = io.StringIO(
  333. '''
  334. stuff:
  335. - foo: bar
  336. baz: quux
  337. '''
  338. )
  339. include_file.name = 'include.yaml'
  340. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  341. config_file = io.StringIO(
  342. '''
  343. stuff:
  344. - !omit foo: bar
  345. baz: quux
  346. <<: !include include.yaml
  347. '''
  348. )
  349. config_file.name = 'config.yaml'
  350. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  351. with pytest.raises(ValueError):
  352. module.load_configuration('config.yaml')
  353. def test_load_configuration_with_omit_tag_but_without_merge_raises():
  354. builtins = flexmock(sys.modules['builtins'])
  355. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  356. flexmock(module.os.path).should_receive('isabs').and_return(False)
  357. flexmock(module.os.path).should_receive('exists').and_return(True)
  358. include_file = io.StringIO(
  359. '''
  360. stuff:
  361. - a
  362. - !omit b
  363. - c
  364. '''
  365. )
  366. include_file.name = 'include.yaml'
  367. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  368. config_file = io.StringIO(
  369. '''
  370. stuff:
  371. - x
  372. - y
  373. <<: !include include.yaml
  374. '''
  375. )
  376. config_file.name = 'config.yaml'
  377. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  378. with pytest.raises(ValueError):
  379. module.load_configuration('config.yaml')
  380. def test_load_configuration_does_not_merge_include_list():
  381. builtins = flexmock(sys.modules['builtins'])
  382. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  383. flexmock(module.os.path).should_receive('isabs').and_return(False)
  384. flexmock(module.os.path).should_receive('exists').and_return(True)
  385. include_file = io.StringIO(
  386. '''
  387. - one
  388. - two
  389. '''
  390. )
  391. include_file.name = 'include.yaml'
  392. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  393. config_file = io.StringIO(
  394. '''
  395. foo: bar
  396. repositories:
  397. <<: !include include.yaml
  398. '''
  399. )
  400. config_file.name = 'config.yaml'
  401. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  402. with pytest.raises(module.ruamel.yaml.error.YAMLError):
  403. assert module.load_configuration('config.yaml')
  404. @pytest.mark.parametrize(
  405. 'node_class',
  406. (
  407. module.ruamel.yaml.nodes.MappingNode,
  408. module.ruamel.yaml.nodes.SequenceNode,
  409. module.ruamel.yaml.nodes.ScalarNode,
  410. ),
  411. )
  412. def test_raise_retain_node_error_raises(node_class):
  413. with pytest.raises(ValueError):
  414. module.raise_retain_node_error(
  415. loader=flexmock(), node=node_class(tag=flexmock(), value=flexmock())
  416. )
  417. def test_raise_omit_node_error_raises():
  418. with pytest.raises(ValueError):
  419. module.raise_omit_node_error(loader=flexmock(), node=flexmock())
  420. def test_filter_omitted_nodes_discards_values_with_omit_tag_and_also_equal_values():
  421. nodes = [flexmock(), flexmock()]
  422. values = [
  423. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='a'),
  424. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='b'),
  425. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
  426. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='a'),
  427. module.ruamel.yaml.nodes.ScalarNode(tag='!omit', value='b'),
  428. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
  429. ]
  430. result = module.filter_omitted_nodes(nodes, values)
  431. assert [item.value for item in result] == ['a', 'c', 'a', 'c']
  432. def test_filter_omitted_nodes_keeps_all_values_when_given_only_one_node():
  433. nodes = [flexmock()]
  434. values = [
  435. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='a'),
  436. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='b'),
  437. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
  438. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='a'),
  439. module.ruamel.yaml.nodes.ScalarNode(tag='!omit', value='b'),
  440. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
  441. ]
  442. result = module.filter_omitted_nodes(nodes, values)
  443. assert [item.value for item in result] == ['a', 'b', 'c', 'a', 'b', 'c']
  444. def test_merge_values_combines_mapping_values():
  445. nodes = [
  446. (
  447. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
  448. module.ruamel.yaml.nodes.MappingNode(
  449. tag='tag:yaml.org,2002:map',
  450. value=[
  451. (
  452. module.ruamel.yaml.nodes.ScalarNode(
  453. tag='tag:yaml.org,2002:str', value='keep_hourly'
  454. ),
  455. module.ruamel.yaml.nodes.ScalarNode(
  456. tag='tag:yaml.org,2002:int', value='24'
  457. ),
  458. ),
  459. (
  460. module.ruamel.yaml.nodes.ScalarNode(
  461. tag='tag:yaml.org,2002:str', value='keep_daily'
  462. ),
  463. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
  464. ),
  465. ],
  466. ),
  467. ),
  468. (
  469. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
  470. module.ruamel.yaml.nodes.MappingNode(
  471. tag='tag:yaml.org,2002:map',
  472. value=[
  473. (
  474. module.ruamel.yaml.nodes.ScalarNode(
  475. tag='tag:yaml.org,2002:str', value='keep_daily'
  476. ),
  477. module.ruamel.yaml.nodes.ScalarNode(
  478. tag='tag:yaml.org,2002:int', value='25'
  479. ),
  480. ),
  481. ],
  482. ),
  483. ),
  484. (
  485. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
  486. module.ruamel.yaml.nodes.MappingNode(
  487. tag='tag:yaml.org,2002:map',
  488. value=[
  489. (
  490. module.ruamel.yaml.nodes.ScalarNode(
  491. tag='tag:yaml.org,2002:str', value='keep_nanosecondly'
  492. ),
  493. module.ruamel.yaml.nodes.ScalarNode(
  494. tag='tag:yaml.org,2002:int', value='1000'
  495. ),
  496. ),
  497. ],
  498. ),
  499. ),
  500. ]
  501. values = module.merge_values(nodes)
  502. assert len(values) == 4
  503. assert values[0][0].value == 'keep_hourly'
  504. assert values[0][1].value == '24'
  505. assert values[1][0].value == 'keep_daily'
  506. assert values[1][1].value == '7'
  507. assert values[2][0].value == 'keep_daily'
  508. assert values[2][1].value == '25'
  509. assert values[3][0].value == 'keep_nanosecondly'
  510. assert values[3][1].value == '1000'
  511. def test_merge_values_combines_sequence_values():
  512. nodes = [
  513. (
  514. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
  515. module.ruamel.yaml.nodes.SequenceNode(
  516. tag='tag:yaml.org,2002:seq',
  517. value=[
  518. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='1'),
  519. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='2'),
  520. ],
  521. ),
  522. ),
  523. (
  524. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
  525. module.ruamel.yaml.nodes.SequenceNode(
  526. tag='tag:yaml.org,2002:seq',
  527. value=[
  528. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='3'),
  529. ],
  530. ),
  531. ),
  532. (
  533. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
  534. module.ruamel.yaml.nodes.SequenceNode(
  535. tag='tag:yaml.org,2002:seq',
  536. value=[
  537. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='4'),
  538. ],
  539. ),
  540. ),
  541. ]
  542. values = module.merge_values(nodes)
  543. assert len(values) == 4
  544. assert values[0].value == '1'
  545. assert values[1].value == '2'
  546. assert values[2].value == '3'
  547. assert values[3].value == '4'
  548. def test_deep_merge_nodes_replaces_colliding_scalar_values():
  549. node_values = [
  550. (
  551. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  552. module.ruamel.yaml.nodes.MappingNode(
  553. tag='tag:yaml.org,2002:map',
  554. value=[
  555. (
  556. module.ruamel.yaml.nodes.ScalarNode(
  557. tag='tag:yaml.org,2002:str', value='keep_hourly'
  558. ),
  559. module.ruamel.yaml.nodes.ScalarNode(
  560. tag='tag:yaml.org,2002:int', value='24'
  561. ),
  562. ),
  563. (
  564. module.ruamel.yaml.nodes.ScalarNode(
  565. tag='tag:yaml.org,2002:str', value='keep_daily'
  566. ),
  567. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
  568. ),
  569. ],
  570. ),
  571. ),
  572. (
  573. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  574. module.ruamel.yaml.nodes.MappingNode(
  575. tag='tag:yaml.org,2002:map',
  576. value=[
  577. (
  578. module.ruamel.yaml.nodes.ScalarNode(
  579. tag='tag:yaml.org,2002:str', value='keep_daily'
  580. ),
  581. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
  582. ),
  583. ],
  584. ),
  585. ),
  586. ]
  587. result = module.deep_merge_nodes(node_values)
  588. assert len(result) == 1
  589. (section_key, section_value) = result[0]
  590. assert section_key.value == 'retention'
  591. options = section_value.value
  592. assert len(options) == 2
  593. assert options[0][0].value == 'keep_daily'
  594. assert options[0][1].value == '5'
  595. assert options[1][0].value == 'keep_hourly'
  596. assert options[1][1].value == '24'
  597. def test_deep_merge_nodes_keeps_non_colliding_scalar_values():
  598. node_values = [
  599. (
  600. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  601. module.ruamel.yaml.nodes.MappingNode(
  602. tag='tag:yaml.org,2002:map',
  603. value=[
  604. (
  605. module.ruamel.yaml.nodes.ScalarNode(
  606. tag='tag:yaml.org,2002:str', value='keep_hourly'
  607. ),
  608. module.ruamel.yaml.nodes.ScalarNode(
  609. tag='tag:yaml.org,2002:int', value='24'
  610. ),
  611. ),
  612. (
  613. module.ruamel.yaml.nodes.ScalarNode(
  614. tag='tag:yaml.org,2002:str', value='keep_daily'
  615. ),
  616. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
  617. ),
  618. ],
  619. ),
  620. ),
  621. (
  622. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  623. module.ruamel.yaml.nodes.MappingNode(
  624. tag='tag:yaml.org,2002:map',
  625. value=[
  626. (
  627. module.ruamel.yaml.nodes.ScalarNode(
  628. tag='tag:yaml.org,2002:str', value='keep_minutely'
  629. ),
  630. module.ruamel.yaml.nodes.ScalarNode(
  631. tag='tag:yaml.org,2002:int', value='10'
  632. ),
  633. ),
  634. ],
  635. ),
  636. ),
  637. ]
  638. result = module.deep_merge_nodes(node_values)
  639. assert len(result) == 1
  640. (section_key, section_value) = result[0]
  641. assert section_key.value == 'retention'
  642. options = section_value.value
  643. assert len(options) == 3
  644. assert options[0][0].value == 'keep_daily'
  645. assert options[0][1].value == '7'
  646. assert options[1][0].value == 'keep_hourly'
  647. assert options[1][1].value == '24'
  648. assert options[2][0].value == 'keep_minutely'
  649. assert options[2][1].value == '10'
  650. def test_deep_merge_nodes_keeps_deeply_nested_values():
  651. node_values = [
  652. (
  653. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'),
  654. module.ruamel.yaml.nodes.MappingNode(
  655. tag='tag:yaml.org,2002:map',
  656. value=[
  657. (
  658. module.ruamel.yaml.nodes.ScalarNode(
  659. tag='tag:yaml.org,2002:str', value='lock_wait'
  660. ),
  661. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
  662. ),
  663. (
  664. module.ruamel.yaml.nodes.ScalarNode(
  665. tag='tag:yaml.org,2002:str', value='extra_borg_options'
  666. ),
  667. module.ruamel.yaml.nodes.MappingNode(
  668. tag='tag:yaml.org,2002:map',
  669. value=[
  670. (
  671. module.ruamel.yaml.nodes.ScalarNode(
  672. tag='tag:yaml.org,2002:str', value='init'
  673. ),
  674. module.ruamel.yaml.nodes.ScalarNode(
  675. tag='tag:yaml.org,2002:str', value='--init-option'
  676. ),
  677. ),
  678. ],
  679. ),
  680. ),
  681. ],
  682. ),
  683. ),
  684. (
  685. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'),
  686. module.ruamel.yaml.nodes.MappingNode(
  687. tag='tag:yaml.org,2002:map',
  688. value=[
  689. (
  690. module.ruamel.yaml.nodes.ScalarNode(
  691. tag='tag:yaml.org,2002:str', value='extra_borg_options'
  692. ),
  693. module.ruamel.yaml.nodes.MappingNode(
  694. tag='tag:yaml.org,2002:map',
  695. value=[
  696. (
  697. module.ruamel.yaml.nodes.ScalarNode(
  698. tag='tag:yaml.org,2002:str', value='prune'
  699. ),
  700. module.ruamel.yaml.nodes.ScalarNode(
  701. tag='tag:yaml.org,2002:str', value='--prune-option'
  702. ),
  703. ),
  704. ],
  705. ),
  706. ),
  707. ],
  708. ),
  709. ),
  710. ]
  711. result = module.deep_merge_nodes(node_values)
  712. assert len(result) == 1
  713. (section_key, section_value) = result[0]
  714. assert section_key.value == 'storage'
  715. options = section_value.value
  716. assert len(options) == 2
  717. assert options[0][0].value == 'extra_borg_options'
  718. assert options[1][0].value == 'lock_wait'
  719. assert options[1][1].value == '5'
  720. nested_options = options[0][1].value
  721. assert len(nested_options) == 2
  722. assert nested_options[0][0].value == 'init'
  723. assert nested_options[0][1].value == '--init-option'
  724. assert nested_options[1][0].value == 'prune'
  725. assert nested_options[1][1].value == '--prune-option'
  726. def test_deep_merge_nodes_appends_colliding_sequence_values():
  727. node_values = [
  728. (
  729. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  730. module.ruamel.yaml.nodes.MappingNode(
  731. tag='tag:yaml.org,2002:map',
  732. value=[
  733. (
  734. module.ruamel.yaml.nodes.ScalarNode(
  735. tag='tag:yaml.org,2002:str', value='before_backup'
  736. ),
  737. module.ruamel.yaml.nodes.SequenceNode(
  738. tag='tag:yaml.org,2002:seq',
  739. value=[
  740. module.ruamel.yaml.ScalarNode(
  741. tag='tag:yaml.org,2002:str', value='echo 1'
  742. ),
  743. module.ruamel.yaml.ScalarNode(
  744. tag='tag:yaml.org,2002:str', value='echo 2'
  745. ),
  746. ],
  747. ),
  748. ),
  749. ],
  750. ),
  751. ),
  752. (
  753. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  754. module.ruamel.yaml.nodes.MappingNode(
  755. tag='tag:yaml.org,2002:map',
  756. value=[
  757. (
  758. module.ruamel.yaml.nodes.ScalarNode(
  759. tag='tag:yaml.org,2002:str', value='before_backup'
  760. ),
  761. module.ruamel.yaml.nodes.SequenceNode(
  762. tag='tag:yaml.org,2002:seq',
  763. value=[
  764. module.ruamel.yaml.ScalarNode(
  765. tag='tag:yaml.org,2002:str', value='echo 3'
  766. ),
  767. module.ruamel.yaml.ScalarNode(
  768. tag='tag:yaml.org,2002:str', value='echo 4'
  769. ),
  770. ],
  771. ),
  772. ),
  773. ],
  774. ),
  775. ),
  776. ]
  777. result = module.deep_merge_nodes(node_values)
  778. assert len(result) == 1
  779. (section_key, section_value) = result[0]
  780. assert section_key.value == 'hooks'
  781. options = section_value.value
  782. assert len(options) == 1
  783. assert options[0][0].value == 'before_backup'
  784. assert [item.value for item in options[0][1].value] == ['echo 1', 'echo 2', 'echo 3', 'echo 4']
  785. def test_deep_merge_nodes_errors_on_colliding_values_of_different_types():
  786. node_values = [
  787. (
  788. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  789. module.ruamel.yaml.nodes.MappingNode(
  790. tag='tag:yaml.org,2002:map',
  791. value=[
  792. (
  793. module.ruamel.yaml.nodes.ScalarNode(
  794. tag='tag:yaml.org,2002:str', value='before_backup'
  795. ),
  796. module.ruamel.yaml.nodes.ScalarNode(
  797. tag='tag:yaml.org,2002:str', value='echo oopsie daisy'
  798. ),
  799. ),
  800. ],
  801. ),
  802. ),
  803. (
  804. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  805. module.ruamel.yaml.nodes.MappingNode(
  806. tag='tag:yaml.org,2002:map',
  807. value=[
  808. (
  809. module.ruamel.yaml.nodes.ScalarNode(
  810. tag='tag:yaml.org,2002:str', value='before_backup'
  811. ),
  812. module.ruamel.yaml.nodes.SequenceNode(
  813. tag='tag:yaml.org,2002:seq',
  814. value=[
  815. module.ruamel.yaml.ScalarNode(
  816. tag='tag:yaml.org,2002:str', value='echo 3'
  817. ),
  818. module.ruamel.yaml.ScalarNode(
  819. tag='tag:yaml.org,2002:str', value='echo 4'
  820. ),
  821. ],
  822. ),
  823. ),
  824. ],
  825. ),
  826. ),
  827. ]
  828. with pytest.raises(ValueError):
  829. module.deep_merge_nodes(node_values)
  830. def test_deep_merge_nodes_only_keeps_mapping_values_tagged_with_retain():
  831. node_values = [
  832. (
  833. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  834. module.ruamel.yaml.nodes.MappingNode(
  835. tag='tag:yaml.org,2002:map',
  836. value=[
  837. (
  838. module.ruamel.yaml.nodes.ScalarNode(
  839. tag='tag:yaml.org,2002:str', value='keep_hourly'
  840. ),
  841. module.ruamel.yaml.nodes.ScalarNode(
  842. tag='tag:yaml.org,2002:int', value='24'
  843. ),
  844. ),
  845. (
  846. module.ruamel.yaml.nodes.ScalarNode(
  847. tag='tag:yaml.org,2002:str', value='keep_daily'
  848. ),
  849. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
  850. ),
  851. ],
  852. ),
  853. ),
  854. (
  855. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  856. module.ruamel.yaml.nodes.MappingNode(
  857. tag='!retain',
  858. value=[
  859. (
  860. module.ruamel.yaml.nodes.ScalarNode(
  861. tag='tag:yaml.org,2002:str', value='keep_daily'
  862. ),
  863. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
  864. ),
  865. ],
  866. ),
  867. ),
  868. ]
  869. result = module.deep_merge_nodes(node_values)
  870. assert len(result) == 1
  871. (section_key, section_value) = result[0]
  872. assert section_key.value == 'retention'
  873. assert section_value.tag == 'tag:yaml.org,2002:map'
  874. options = section_value.value
  875. assert len(options) == 1
  876. assert options[0][0].value == 'keep_daily'
  877. assert options[0][1].value == '5'
  878. def test_deep_merge_nodes_only_keeps_sequence_values_tagged_with_retain():
  879. node_values = [
  880. (
  881. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  882. module.ruamel.yaml.nodes.MappingNode(
  883. tag='tag:yaml.org,2002:map',
  884. value=[
  885. (
  886. module.ruamel.yaml.nodes.ScalarNode(
  887. tag='tag:yaml.org,2002:str', value='before_backup'
  888. ),
  889. module.ruamel.yaml.nodes.SequenceNode(
  890. tag='tag:yaml.org,2002:seq',
  891. value=[
  892. module.ruamel.yaml.ScalarNode(
  893. tag='tag:yaml.org,2002:str', value='echo 1'
  894. ),
  895. module.ruamel.yaml.ScalarNode(
  896. tag='tag:yaml.org,2002:str', value='echo 2'
  897. ),
  898. ],
  899. ),
  900. ),
  901. ],
  902. ),
  903. ),
  904. (
  905. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  906. module.ruamel.yaml.nodes.MappingNode(
  907. tag='tag:yaml.org,2002:map',
  908. value=[
  909. (
  910. module.ruamel.yaml.nodes.ScalarNode(
  911. tag='tag:yaml.org,2002:str', value='before_backup'
  912. ),
  913. module.ruamel.yaml.nodes.SequenceNode(
  914. tag='!retain',
  915. value=[
  916. module.ruamel.yaml.ScalarNode(
  917. tag='tag:yaml.org,2002:str', value='echo 3'
  918. ),
  919. module.ruamel.yaml.ScalarNode(
  920. tag='tag:yaml.org,2002:str', value='echo 4'
  921. ),
  922. ],
  923. ),
  924. ),
  925. ],
  926. ),
  927. ),
  928. ]
  929. result = module.deep_merge_nodes(node_values)
  930. assert len(result) == 1
  931. (section_key, section_value) = result[0]
  932. assert section_key.value == 'hooks'
  933. options = section_value.value
  934. assert len(options) == 1
  935. assert options[0][0].value == 'before_backup'
  936. assert options[0][1].tag == 'tag:yaml.org,2002:seq'
  937. assert [item.value for item in options[0][1].value] == ['echo 3', 'echo 4']
  938. def test_deep_merge_nodes_skips_sequence_values_tagged_with_omit():
  939. node_values = [
  940. (
  941. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  942. module.ruamel.yaml.nodes.MappingNode(
  943. tag='tag:yaml.org,2002:map',
  944. value=[
  945. (
  946. module.ruamel.yaml.nodes.ScalarNode(
  947. tag='tag:yaml.org,2002:str', value='before_backup'
  948. ),
  949. module.ruamel.yaml.nodes.SequenceNode(
  950. tag='tag:yaml.org,2002:seq',
  951. value=[
  952. module.ruamel.yaml.ScalarNode(
  953. tag='tag:yaml.org,2002:str', value='echo 1'
  954. ),
  955. module.ruamel.yaml.ScalarNode(
  956. tag='tag:yaml.org,2002:str', value='echo 2'
  957. ),
  958. ],
  959. ),
  960. ),
  961. ],
  962. ),
  963. ),
  964. (
  965. module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  966. module.ruamel.yaml.nodes.MappingNode(
  967. tag='tag:yaml.org,2002:map',
  968. value=[
  969. (
  970. module.ruamel.yaml.nodes.ScalarNode(
  971. tag='tag:yaml.org,2002:str', value='before_backup'
  972. ),
  973. module.ruamel.yaml.nodes.SequenceNode(
  974. tag='tag:yaml.org,2002:seq',
  975. value=[
  976. module.ruamel.yaml.ScalarNode(tag='!omit', value='echo 2'),
  977. module.ruamel.yaml.ScalarNode(
  978. tag='tag:yaml.org,2002:str', value='echo 3'
  979. ),
  980. ],
  981. ),
  982. ),
  983. ],
  984. ),
  985. ),
  986. ]
  987. result = module.deep_merge_nodes(node_values)
  988. assert len(result) == 1
  989. (section_key, section_value) = result[0]
  990. assert section_key.value == 'hooks'
  991. options = section_value.value
  992. assert len(options) == 1
  993. assert options[0][0].value == 'before_backup'
  994. assert [item.value for item in options[0][1].value] == ['echo 1', 'echo 3']