test_normalize.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. import pytest
  2. from flexmock import flexmock
  3. from borgmatic.config import normalize as module
  4. @pytest.mark.parametrize(
  5. 'config,expected_config,produces_logs',
  6. (
  7. (
  8. {'location': {'foo': 'bar', 'baz': 'quux'}},
  9. {'foo': 'bar', 'baz': 'quux'},
  10. True,
  11. ),
  12. (
  13. {'retention': {'foo': 'bar', 'baz': 'quux'}},
  14. {'foo': 'bar', 'baz': 'quux'},
  15. True,
  16. ),
  17. (
  18. {'consistency': {'foo': 'bar', 'baz': 'quux'}},
  19. {'foo': 'bar', 'baz': 'quux'},
  20. True,
  21. ),
  22. (
  23. {'output': {'foo': 'bar', 'baz': 'quux'}},
  24. {'foo': 'bar', 'baz': 'quux'},
  25. True,
  26. ),
  27. (
  28. {'hooks': {'foo': 'bar', 'baz': 'quux'}},
  29. {'foo': 'bar', 'baz': 'quux'},
  30. True,
  31. ),
  32. (
  33. {'location': {'foo': 'bar'}, 'storage': {'baz': 'quux'}},
  34. {'foo': 'bar', 'baz': 'quux'},
  35. True,
  36. ),
  37. (
  38. {'foo': 'bar', 'baz': 'quux'},
  39. {'foo': 'bar', 'baz': 'quux'},
  40. False,
  41. ),
  42. (
  43. {'location': {'prefix': 'foo'}, 'consistency': {'prefix': 'foo'}},
  44. {'prefix': 'foo'},
  45. True,
  46. ),
  47. (
  48. {'location': {'prefix': 'foo'}, 'consistency': {'prefix': 'foo'}},
  49. {'prefix': 'foo'},
  50. True,
  51. ),
  52. (
  53. {'location': {'prefix': 'foo'}, 'consistency': {'bar': 'baz'}},
  54. {'prefix': 'foo', 'bar': 'baz'},
  55. True,
  56. ),
  57. (
  58. {'storage': {'umask': 'foo'}, 'hooks': {'umask': 'foo'}},
  59. {'umask': 'foo'},
  60. True,
  61. ),
  62. (
  63. {'storage': {'umask': 'foo'}, 'hooks': {'umask': 'foo'}},
  64. {'umask': 'foo'},
  65. True,
  66. ),
  67. (
  68. {'storage': {'umask': 'foo'}, 'hooks': {'bar': 'baz'}},
  69. {'umask': 'foo', 'bar': 'baz'},
  70. True,
  71. ),
  72. (
  73. {'location': {'bar': 'baz'}, 'consistency': {'prefix': 'foo'}},
  74. {'bar': 'baz', 'prefix': 'foo'},
  75. True,
  76. ),
  77. (
  78. {'location': {}, 'consistency': {'prefix': 'foo'}},
  79. {'prefix': 'foo'},
  80. True,
  81. ),
  82. (
  83. {},
  84. {},
  85. False,
  86. ),
  87. ),
  88. )
  89. def test_normalize_sections_moves_section_options_to_global_scope(
  90. config, expected_config, produces_logs
  91. ):
  92. logs = module.normalize_sections('test.yaml', config)
  93. assert config == expected_config
  94. if produces_logs:
  95. assert logs
  96. else:
  97. assert logs == []
  98. def test_normalize_sections_with_different_prefix_values_raises():
  99. config = {'location': {'prefix': 'foo'}, 'consistency': {'prefix': 'bar'}}
  100. with pytest.raises(ValueError):
  101. module.normalize_sections('test.yaml', config)
  102. def test_normalize_sections_with_different_umask_values_raises():
  103. config = {'storage': {'umask': 'foo'}, 'hooks': {'umask': 'bar'}}
  104. with pytest.raises(ValueError):
  105. module.normalize_sections('test.yaml', config)
  106. def test_normalize_sections_with_only_scalar_raises():
  107. config = 33
  108. with pytest.raises(ValueError):
  109. module.normalize_sections('test.yaml', config)
  110. @pytest.mark.parametrize(
  111. 'config,expected_config,produces_logs',
  112. (
  113. (
  114. {'before_actions': ['foo', 'bar'], 'after_actions': ['baz']},
  115. {
  116. 'commands': [
  117. {'before': 'repository', 'run': ['foo', 'bar']},
  118. {'after': 'repository', 'run': ['baz']},
  119. ]
  120. },
  121. True,
  122. ),
  123. (
  124. {'before_backup': ['foo', 'bar'], 'after_backup': ['baz']},
  125. {
  126. 'commands': [
  127. {'before': 'action', 'when': ['create'], 'run': ['foo', 'bar']},
  128. {'after': 'action', 'when': ['create'], 'run': ['baz']},
  129. ]
  130. },
  131. True,
  132. ),
  133. (
  134. {'before_prune': ['foo', 'bar'], 'after_prune': ['baz']},
  135. {
  136. 'commands': [
  137. {'before': 'action', 'when': ['prune'], 'run': ['foo', 'bar']},
  138. {'after': 'action', 'when': ['prune'], 'run': ['baz']},
  139. ]
  140. },
  141. True,
  142. ),
  143. (
  144. {'before_compact': ['foo', 'bar'], 'after_compact': ['baz']},
  145. {
  146. 'commands': [
  147. {'before': 'action', 'when': ['compact'], 'run': ['foo', 'bar']},
  148. {'after': 'action', 'when': ['compact'], 'run': ['baz']},
  149. ]
  150. },
  151. True,
  152. ),
  153. (
  154. {'before_check': ['foo', 'bar'], 'after_check': ['baz']},
  155. {
  156. 'commands': [
  157. {'before': 'action', 'when': ['check'], 'run': ['foo', 'bar']},
  158. {'after': 'action', 'when': ['check'], 'run': ['baz']},
  159. ]
  160. },
  161. True,
  162. ),
  163. (
  164. {'before_extract': ['foo', 'bar'], 'after_extract': ['baz']},
  165. {
  166. 'commands': [
  167. {'before': 'action', 'when': ['extract'], 'run': ['foo', 'bar']},
  168. {'after': 'action', 'when': ['extract'], 'run': ['baz']},
  169. ]
  170. },
  171. True,
  172. ),
  173. (
  174. {'on_error': ['foo', 'bar']},
  175. {
  176. 'commands': [
  177. {
  178. 'after': 'error',
  179. 'when': ['create', 'prune', 'compact', 'check'],
  180. 'run': ['foo', 'bar'],
  181. },
  182. ]
  183. },
  184. True,
  185. ),
  186. (
  187. {'before_everything': ['foo', 'bar'], 'after_everything': ['baz']},
  188. {
  189. 'commands': [
  190. {'before': 'everything', 'when': ['create'], 'run': ['foo', 'bar']},
  191. {'after': 'everything', 'when': ['create'], 'run': ['baz']},
  192. ]
  193. },
  194. True,
  195. ),
  196. (
  197. {'other': 'options', 'unrelated_to': 'commands'},
  198. {'other': 'options', 'unrelated_to': 'commands'},
  199. False,
  200. ),
  201. ),
  202. )
  203. def test_normalize_commands_moves_individual_command_hooks_to_unified_commands(
  204. config, expected_config, produces_logs
  205. ):
  206. flexmock(module).should_receive('make_command_hook_deprecation_log').and_return(flexmock())
  207. logs = module.normalize_commands('test.yaml', config)
  208. assert config == expected_config
  209. if produces_logs:
  210. assert logs
  211. else:
  212. assert logs == []
  213. @pytest.mark.parametrize(
  214. 'config,expected_config,produces_logs',
  215. (
  216. (
  217. {'exclude_if_present': '.nobackup'},
  218. {'exclude_if_present': ['.nobackup']},
  219. True,
  220. ),
  221. (
  222. {'exclude_if_present': ['.nobackup']},
  223. {'exclude_if_present': ['.nobackup']},
  224. False,
  225. ),
  226. (
  227. {'store_config_files': False},
  228. {'bootstrap': {'store_config_files': False}},
  229. True,
  230. ),
  231. (
  232. {'source_directories': ['foo', 'bar']},
  233. {'source_directories': ['foo', 'bar']},
  234. False,
  235. ),
  236. (
  237. {'compression': 'yes_please'},
  238. {'compression': 'yes_please'},
  239. False,
  240. ),
  241. (
  242. {'healthchecks': 'https://example.com'},
  243. {'healthchecks': {'ping_url': 'https://example.com'}},
  244. True,
  245. ),
  246. (
  247. {'cronitor': 'https://example.com'},
  248. {'cronitor': {'ping_url': 'https://example.com'}},
  249. True,
  250. ),
  251. (
  252. {'pagerduty': 'https://example.com'},
  253. {'pagerduty': {'integration_key': 'https://example.com'}},
  254. True,
  255. ),
  256. (
  257. {'cronhub': 'https://example.com'},
  258. {'cronhub': {'ping_url': 'https://example.com'}},
  259. True,
  260. ),
  261. (
  262. {'checks': ['archives']},
  263. {'checks': [{'name': 'archives'}]},
  264. True,
  265. ),
  266. (
  267. {'checks': ['archives']},
  268. {'checks': [{'name': 'archives'}]},
  269. True,
  270. ),
  271. (
  272. {'numeric_owner': False},
  273. {'numeric_ids': False},
  274. True,
  275. ),
  276. (
  277. {'bsd_flags': False},
  278. {'flags': False},
  279. True,
  280. ),
  281. (
  282. {'remote_rate_limit': False},
  283. {'upload_rate_limit': False},
  284. True,
  285. ),
  286. (
  287. {'repositories': ['foo@bar:/repo']},
  288. {'repositories': [{'path': 'ssh://foo@bar/repo'}]},
  289. True,
  290. ),
  291. (
  292. {'repositories': ['foo@bar:repo']},
  293. {'repositories': [{'path': 'ssh://foo@bar/./repo'}]},
  294. True,
  295. ),
  296. (
  297. {'repositories': ['foo@bar:~/repo']},
  298. {'repositories': [{'path': 'ssh://foo@bar/~/repo'}]},
  299. True,
  300. ),
  301. (
  302. {'repositories': ['ssh://foo@bar:1234/repo']},
  303. {'repositories': [{'path': 'ssh://foo@bar:1234/repo'}]},
  304. True,
  305. ),
  306. (
  307. {'repositories': ['sftp://foo@bar:1234/repo']},
  308. {'repositories': [{'path': 'sftp://foo@bar:1234/repo'}]},
  309. True,
  310. ),
  311. (
  312. {'repositories': ['rclone:host:repo']},
  313. {'repositories': [{'path': 'rclone:host:repo'}]},
  314. True,
  315. ),
  316. (
  317. {'repositories': ['s3:stuff']},
  318. {'repositories': [{'path': 's3:stuff'}]},
  319. True,
  320. ),
  321. (
  322. {'repositories': ['b2:stuff']},
  323. {'repositories': [{'path': 'b2:stuff'}]},
  324. True,
  325. ),
  326. (
  327. {'repositories': ['file:///repo']},
  328. {'repositories': [{'path': '/repo'}]},
  329. True,
  330. ),
  331. (
  332. {'repositories': [{'path': 'first'}, 'file:///repo']},
  333. {'repositories': [{'path': 'first'}, {'path': '/repo'}]},
  334. True,
  335. ),
  336. (
  337. {'repositories': [{'path': 'foo@bar:/repo', 'label': 'foo'}]},
  338. {'repositories': [{'path': 'ssh://foo@bar/repo', 'label': 'foo'}]},
  339. True,
  340. ),
  341. (
  342. {'repositories': [{'path': 'file:///repo', 'label': 'foo'}]},
  343. {'repositories': [{'path': '/repo', 'label': 'foo'}]},
  344. False,
  345. ),
  346. (
  347. {'repositories': [{'path': '/repo', 'label': 'foo'}]},
  348. {'repositories': [{'path': '/repo', 'label': 'foo'}]},
  349. False,
  350. ),
  351. (
  352. {'repositories': [{'path': None, 'label': 'foo'}]},
  353. {'repositories': []},
  354. False,
  355. ),
  356. (
  357. {'prefix': 'foo'},
  358. {'prefix': 'foo'},
  359. True,
  360. ),
  361. ),
  362. )
  363. def test_normalize_applies_hard_coded_normalization_to_config(
  364. config, expected_config, produces_logs
  365. ):
  366. flexmock(module).should_receive('normalize_sections').and_return([])
  367. flexmock(module).should_receive('normalize_commands').and_return([])
  368. logs = module.normalize('test.yaml', config)
  369. expected_config.setdefault('bootstrap', {})
  370. assert config == expected_config
  371. if produces_logs:
  372. assert logs
  373. else:
  374. assert logs == []
  375. def test_normalize_config_with_borgmatic_source_directory_warns():
  376. flexmock(module).should_receive('normalize_sections').and_return([])
  377. flexmock(module).should_receive('normalize_commands').and_return([])
  378. logs = module.normalize('test.yaml', {'borgmatic_source_directory': '~/.borgmatic'})
  379. assert len(logs) == 1
  380. assert logs[0].levelno == module.logging.WARNING
  381. assert 'borgmatic_source_directory' in logs[0].msg