2
0

test_load.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import io
  2. import sys
  3. import pytest
  4. import ruamel.yaml
  5. from flexmock import flexmock
  6. from borgmatic.config import load as module
  7. def test_load_configuration_parses_contents():
  8. builtins = flexmock(sys.modules['builtins'])
  9. config_file = io.StringIO('key: value')
  10. config_file.name = 'config.yaml'
  11. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  12. assert module.load_configuration('config.yaml') == {'key': 'value'}
  13. def test_load_configuration_replaces_constants():
  14. builtins = flexmock(sys.modules['builtins'])
  15. config_file = io.StringIO(
  16. '''
  17. constants:
  18. key: value
  19. key: {key}
  20. '''
  21. )
  22. config_file.name = 'config.yaml'
  23. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  24. assert module.load_configuration('config.yaml') == {'key': 'value'}
  25. def test_load_configuration_replaces_complex_constants():
  26. builtins = flexmock(sys.modules['builtins'])
  27. config_file = io.StringIO(
  28. '''
  29. constants:
  30. key:
  31. subkey: value
  32. key: {key}
  33. '''
  34. )
  35. config_file.name = 'config.yaml'
  36. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  37. assert module.load_configuration('config.yaml') == {'key': {'subkey': 'value'}}
  38. def test_load_configuration_inlines_include_relative_to_current_directory():
  39. builtins = flexmock(sys.modules['builtins'])
  40. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  41. flexmock(module.os.path).should_receive('isabs').and_return(False)
  42. flexmock(module.os.path).should_receive('exists').and_return(True)
  43. include_file = io.StringIO('value')
  44. include_file.name = 'include.yaml'
  45. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  46. config_file = io.StringIO('key: !include include.yaml')
  47. config_file.name = 'config.yaml'
  48. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  49. assert module.load_configuration('config.yaml') == {'key': 'value'}
  50. def test_load_configuration_inlines_include_relative_to_config_parent_directory():
  51. builtins = flexmock(sys.modules['builtins'])
  52. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  53. flexmock(module.os.path).should_receive('isabs').with_args('/etc').and_return(True)
  54. flexmock(module.os.path).should_receive('isabs').with_args('/etc/config.yaml').and_return(True)
  55. flexmock(module.os.path).should_receive('isabs').with_args('include.yaml').and_return(False)
  56. flexmock(module.os.path).should_receive('exists').with_args('/tmp/include.yaml').and_return(
  57. False
  58. )
  59. flexmock(module.os.path).should_receive('exists').with_args('/etc/include.yaml').and_return(
  60. True
  61. )
  62. include_file = io.StringIO('value')
  63. include_file.name = 'include.yaml'
  64. builtins.should_receive('open').with_args('/etc/include.yaml').and_return(include_file)
  65. config_file = io.StringIO('key: !include include.yaml')
  66. config_file.name = '/etc/config.yaml'
  67. builtins.should_receive('open').with_args('/etc/config.yaml').and_return(config_file)
  68. assert module.load_configuration('/etc/config.yaml') == {'key': 'value'}
  69. def test_load_configuration_raises_if_relative_include_does_not_exist():
  70. builtins = flexmock(sys.modules['builtins'])
  71. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  72. flexmock(module.os.path).should_receive('isabs').with_args('/etc').and_return(True)
  73. flexmock(module.os.path).should_receive('isabs').with_args('/etc/config.yaml').and_return(True)
  74. flexmock(module.os.path).should_receive('isabs').with_args('include.yaml').and_return(False)
  75. flexmock(module.os.path).should_receive('exists').and_return(False)
  76. config_file = io.StringIO('key: !include include.yaml')
  77. config_file.name = '/etc/config.yaml'
  78. builtins.should_receive('open').with_args('/etc/config.yaml').and_return(config_file)
  79. with pytest.raises(FileNotFoundError):
  80. module.load_configuration('/etc/config.yaml')
  81. def test_load_configuration_inlines_absolute_include():
  82. builtins = flexmock(sys.modules['builtins'])
  83. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  84. flexmock(module.os.path).should_receive('isabs').and_return(True)
  85. flexmock(module.os.path).should_receive('exists').never()
  86. include_file = io.StringIO('value')
  87. include_file.name = '/root/include.yaml'
  88. builtins.should_receive('open').with_args('/root/include.yaml').and_return(include_file)
  89. config_file = io.StringIO('key: !include /root/include.yaml')
  90. config_file.name = 'config.yaml'
  91. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  92. assert module.load_configuration('config.yaml') == {'key': 'value'}
  93. def test_load_configuration_raises_if_absolute_include_does_not_exist():
  94. builtins = flexmock(sys.modules['builtins'])
  95. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  96. flexmock(module.os.path).should_receive('isabs').and_return(True)
  97. builtins.should_receive('open').with_args('/root/include.yaml').and_raise(FileNotFoundError)
  98. config_file = io.StringIO('key: !include /root/include.yaml')
  99. config_file.name = 'config.yaml'
  100. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  101. with pytest.raises(FileNotFoundError):
  102. assert module.load_configuration('config.yaml')
  103. def test_load_configuration_merges_include():
  104. builtins = flexmock(sys.modules['builtins'])
  105. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  106. flexmock(module.os.path).should_receive('isabs').and_return(False)
  107. flexmock(module.os.path).should_receive('exists').and_return(True)
  108. include_file = io.StringIO(
  109. '''
  110. foo: bar
  111. baz: quux
  112. '''
  113. )
  114. include_file.name = 'include.yaml'
  115. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  116. config_file = io.StringIO(
  117. '''
  118. foo: override
  119. <<: !include include.yaml
  120. '''
  121. )
  122. config_file.name = 'config.yaml'
  123. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  124. assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'}
  125. def test_load_configuration_does_not_merge_include_list():
  126. builtins = flexmock(sys.modules['builtins'])
  127. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  128. flexmock(module.os.path).should_receive('isabs').and_return(False)
  129. flexmock(module.os.path).should_receive('exists').and_return(True)
  130. include_file = io.StringIO(
  131. '''
  132. - one
  133. - two
  134. '''
  135. )
  136. include_file.name = 'include.yaml'
  137. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  138. config_file = io.StringIO(
  139. '''
  140. foo: bar
  141. repositories:
  142. <<: !include include.yaml
  143. '''
  144. )
  145. config_file.name = 'config.yaml'
  146. builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
  147. with pytest.raises(ruamel.yaml.error.YAMLError):
  148. assert module.load_configuration('config.yaml')
  149. def test_deep_merge_nodes_replaces_colliding_scalar_values():
  150. node_values = [
  151. (
  152. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  153. ruamel.yaml.nodes.MappingNode(
  154. tag='tag:yaml.org,2002:map',
  155. value=[
  156. (
  157. ruamel.yaml.nodes.ScalarNode(
  158. tag='tag:yaml.org,2002:str', value='keep_hourly'
  159. ),
  160. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='24'),
  161. ),
  162. (
  163. ruamel.yaml.nodes.ScalarNode(
  164. tag='tag:yaml.org,2002:str', value='keep_daily'
  165. ),
  166. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
  167. ),
  168. ],
  169. ),
  170. ),
  171. (
  172. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  173. ruamel.yaml.nodes.MappingNode(
  174. tag='tag:yaml.org,2002:map',
  175. value=[
  176. (
  177. ruamel.yaml.nodes.ScalarNode(
  178. tag='tag:yaml.org,2002:str', value='keep_daily'
  179. ),
  180. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
  181. ),
  182. ],
  183. ),
  184. ),
  185. ]
  186. result = module.deep_merge_nodes(node_values)
  187. assert len(result) == 1
  188. (section_key, section_value) = result[0]
  189. assert section_key.value == 'retention'
  190. options = section_value.value
  191. assert len(options) == 2
  192. assert options[0][0].value == 'keep_hourly'
  193. assert options[0][1].value == '24'
  194. assert options[1][0].value == 'keep_daily'
  195. assert options[1][1].value == '5'
  196. def test_deep_merge_nodes_keeps_non_colliding_scalar_values():
  197. node_values = [
  198. (
  199. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  200. ruamel.yaml.nodes.MappingNode(
  201. tag='tag:yaml.org,2002:map',
  202. value=[
  203. (
  204. ruamel.yaml.nodes.ScalarNode(
  205. tag='tag:yaml.org,2002:str', value='keep_hourly'
  206. ),
  207. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='24'),
  208. ),
  209. (
  210. ruamel.yaml.nodes.ScalarNode(
  211. tag='tag:yaml.org,2002:str', value='keep_daily'
  212. ),
  213. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
  214. ),
  215. ],
  216. ),
  217. ),
  218. (
  219. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
  220. ruamel.yaml.nodes.MappingNode(
  221. tag='tag:yaml.org,2002:map',
  222. value=[
  223. (
  224. ruamel.yaml.nodes.ScalarNode(
  225. tag='tag:yaml.org,2002:str', value='keep_minutely'
  226. ),
  227. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='10'),
  228. ),
  229. ],
  230. ),
  231. ),
  232. ]
  233. result = module.deep_merge_nodes(node_values)
  234. assert len(result) == 1
  235. (section_key, section_value) = result[0]
  236. assert section_key.value == 'retention'
  237. options = section_value.value
  238. assert len(options) == 3
  239. assert options[0][0].value == 'keep_hourly'
  240. assert options[0][1].value == '24'
  241. assert options[1][0].value == 'keep_daily'
  242. assert options[1][1].value == '7'
  243. assert options[2][0].value == 'keep_minutely'
  244. assert options[2][1].value == '10'
  245. def test_deep_merge_nodes_keeps_deeply_nested_values():
  246. node_values = [
  247. (
  248. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'),
  249. ruamel.yaml.nodes.MappingNode(
  250. tag='tag:yaml.org,2002:map',
  251. value=[
  252. (
  253. ruamel.yaml.nodes.ScalarNode(
  254. tag='tag:yaml.org,2002:str', value='lock_wait'
  255. ),
  256. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
  257. ),
  258. (
  259. ruamel.yaml.nodes.ScalarNode(
  260. tag='tag:yaml.org,2002:str', value='extra_borg_options'
  261. ),
  262. ruamel.yaml.nodes.MappingNode(
  263. tag='tag:yaml.org,2002:map',
  264. value=[
  265. (
  266. ruamel.yaml.nodes.ScalarNode(
  267. tag='tag:yaml.org,2002:str', value='init'
  268. ),
  269. ruamel.yaml.nodes.ScalarNode(
  270. tag='tag:yaml.org,2002:str', value='--init-option'
  271. ),
  272. ),
  273. ],
  274. ),
  275. ),
  276. ],
  277. ),
  278. ),
  279. (
  280. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'),
  281. ruamel.yaml.nodes.MappingNode(
  282. tag='tag:yaml.org,2002:map',
  283. value=[
  284. (
  285. ruamel.yaml.nodes.ScalarNode(
  286. tag='tag:yaml.org,2002:str', value='extra_borg_options'
  287. ),
  288. ruamel.yaml.nodes.MappingNode(
  289. tag='tag:yaml.org,2002:map',
  290. value=[
  291. (
  292. ruamel.yaml.nodes.ScalarNode(
  293. tag='tag:yaml.org,2002:str', value='prune'
  294. ),
  295. ruamel.yaml.nodes.ScalarNode(
  296. tag='tag:yaml.org,2002:str', value='--prune-option'
  297. ),
  298. ),
  299. ],
  300. ),
  301. ),
  302. ],
  303. ),
  304. ),
  305. ]
  306. result = module.deep_merge_nodes(node_values)
  307. assert len(result) == 1
  308. (section_key, section_value) = result[0]
  309. assert section_key.value == 'storage'
  310. options = section_value.value
  311. assert len(options) == 2
  312. assert options[0][0].value == 'lock_wait'
  313. assert options[0][1].value == '5'
  314. assert options[1][0].value == 'extra_borg_options'
  315. nested_options = options[1][1].value
  316. assert len(nested_options) == 2
  317. assert nested_options[0][0].value == 'init'
  318. assert nested_options[0][1].value == '--init-option'
  319. assert nested_options[1][0].value == 'prune'
  320. assert nested_options[1][1].value == '--prune-option'
  321. def test_deep_merge_nodes_appends_colliding_sequence_values():
  322. node_values = [
  323. (
  324. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  325. ruamel.yaml.nodes.MappingNode(
  326. tag='tag:yaml.org,2002:map',
  327. value=[
  328. (
  329. ruamel.yaml.nodes.ScalarNode(
  330. tag='tag:yaml.org,2002:str', value='before_backup'
  331. ),
  332. ruamel.yaml.nodes.SequenceNode(
  333. tag='tag:yaml.org,2002:int', value=['echo 1', 'echo 2']
  334. ),
  335. ),
  336. ],
  337. ),
  338. ),
  339. (
  340. ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
  341. ruamel.yaml.nodes.MappingNode(
  342. tag='tag:yaml.org,2002:map',
  343. value=[
  344. (
  345. ruamel.yaml.nodes.ScalarNode(
  346. tag='tag:yaml.org,2002:str', value='before_backup'
  347. ),
  348. ruamel.yaml.nodes.SequenceNode(
  349. tag='tag:yaml.org,2002:int', value=['echo 3', 'echo 4']
  350. ),
  351. ),
  352. ],
  353. ),
  354. ),
  355. ]
  356. result = module.deep_merge_nodes(node_values)
  357. assert len(result) == 1
  358. (section_key, section_value) = result[0]
  359. assert section_key.value == 'hooks'
  360. options = section_value.value
  361. assert len(options) == 1
  362. assert options[0][0].value == 'before_backup'
  363. assert options[0][1].value == ['echo 1', 'echo 2', 'echo 3', 'echo 4']