test_check.py 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  1. import pytest
  2. from flexmock import flexmock
  3. from borgmatic.actions import check as module
  4. def test_parse_checks_returns_them_as_tuple():
  5. checks = module.parse_checks({'checks': [{'name': 'foo'}, {'name': 'bar'}]})
  6. assert checks == ('foo', 'bar')
  7. def test_parse_checks_with_missing_value_returns_defaults():
  8. checks = module.parse_checks({})
  9. assert checks == ('repository', 'archives')
  10. def test_parse_checks_with_empty_list_returns_defaults():
  11. checks = module.parse_checks({'checks': []})
  12. assert checks == ('repository', 'archives')
  13. def test_parse_checks_with_none_value_returns_defaults():
  14. checks = module.parse_checks({'checks': None})
  15. assert checks == ('repository', 'archives')
  16. def test_parse_checks_with_disabled_returns_no_checks():
  17. checks = module.parse_checks({'checks': [{'name': 'foo'}, {'name': 'disabled'}]})
  18. assert checks == ()
  19. def test_parse_checks_prefers_override_checks_to_configured_checks():
  20. checks = module.parse_checks(
  21. {'checks': [{'name': 'archives'}]}, only_checks=['repository', 'extract']
  22. )
  23. assert checks == ('repository', 'extract')
  24. @pytest.mark.parametrize(
  25. 'frequency,expected_result',
  26. (
  27. (None, None),
  28. ('always', None),
  29. ('1 hour', module.datetime.timedelta(hours=1)),
  30. ('2 hours', module.datetime.timedelta(hours=2)),
  31. ('1 day', module.datetime.timedelta(days=1)),
  32. ('2 days', module.datetime.timedelta(days=2)),
  33. ('1 week', module.datetime.timedelta(weeks=1)),
  34. ('2 weeks', module.datetime.timedelta(weeks=2)),
  35. ('1 month', module.datetime.timedelta(days=30)),
  36. ('2 months', module.datetime.timedelta(days=60)),
  37. ('1 year', module.datetime.timedelta(days=365)),
  38. ('2 years', module.datetime.timedelta(days=365 * 2)),
  39. ),
  40. )
  41. def test_parse_frequency_parses_into_timedeltas(frequency, expected_result):
  42. assert module.parse_frequency(frequency) == expected_result
  43. @pytest.mark.parametrize(
  44. 'frequency',
  45. (
  46. 'sometime',
  47. 'x days',
  48. '3 decades',
  49. ),
  50. )
  51. def test_parse_frequency_raises_on_parse_error(frequency):
  52. with pytest.raises(ValueError):
  53. module.parse_frequency(frequency)
  54. def test_filter_checks_on_frequency_without_config_uses_default_checks():
  55. flexmock(module).should_receive('parse_frequency').and_return(
  56. module.datetime.timedelta(weeks=4)
  57. )
  58. flexmock(module).should_receive('make_check_time_path')
  59. flexmock(module).should_receive('probe_for_check_time').and_return(None)
  60. assert module.filter_checks_on_frequency(
  61. config={},
  62. borg_repository_id='repo',
  63. checks=('repository', 'archives'),
  64. force=False,
  65. archives_check_id='1234',
  66. ) == ('repository', 'archives')
  67. def test_filter_checks_on_frequency_retains_unconfigured_check():
  68. assert module.filter_checks_on_frequency(
  69. config={},
  70. borg_repository_id='repo',
  71. checks=('data',),
  72. force=False,
  73. ) == ('data',)
  74. def test_filter_checks_on_frequency_retains_check_without_frequency():
  75. flexmock(module).should_receive('parse_frequency').and_return(None)
  76. assert module.filter_checks_on_frequency(
  77. config={'checks': [{'name': 'archives'}]},
  78. borg_repository_id='repo',
  79. checks=('archives',),
  80. force=False,
  81. archives_check_id='1234',
  82. ) == ('archives',)
  83. def test_filter_checks_on_frequency_retains_check_with_elapsed_frequency():
  84. flexmock(module).should_receive('parse_frequency').and_return(
  85. module.datetime.timedelta(hours=1)
  86. )
  87. flexmock(module).should_receive('make_check_time_path')
  88. flexmock(module).should_receive('probe_for_check_time').and_return(
  89. module.datetime.datetime(year=module.datetime.MINYEAR, month=1, day=1)
  90. )
  91. assert module.filter_checks_on_frequency(
  92. config={'checks': [{'name': 'archives', 'frequency': '1 hour'}]},
  93. borg_repository_id='repo',
  94. checks=('archives',),
  95. force=False,
  96. archives_check_id='1234',
  97. ) == ('archives',)
  98. def test_filter_checks_on_frequency_retains_check_with_missing_check_time_file():
  99. flexmock(module).should_receive('parse_frequency').and_return(
  100. module.datetime.timedelta(hours=1)
  101. )
  102. flexmock(module).should_receive('make_check_time_path')
  103. flexmock(module).should_receive('probe_for_check_time').and_return(None)
  104. assert module.filter_checks_on_frequency(
  105. config={'checks': [{'name': 'archives', 'frequency': '1 hour'}]},
  106. borg_repository_id='repo',
  107. checks=('archives',),
  108. force=False,
  109. archives_check_id='1234',
  110. ) == ('archives',)
  111. def test_filter_checks_on_frequency_skips_check_with_unelapsed_frequency():
  112. flexmock(module).should_receive('parse_frequency').and_return(
  113. module.datetime.timedelta(hours=1)
  114. )
  115. flexmock(module).should_receive('make_check_time_path')
  116. flexmock(module).should_receive('probe_for_check_time').and_return(
  117. module.datetime.datetime.now()
  118. )
  119. assert (
  120. module.filter_checks_on_frequency(
  121. config={'checks': [{'name': 'archives', 'frequency': '1 hour'}]},
  122. borg_repository_id='repo',
  123. checks=('archives',),
  124. force=False,
  125. archives_check_id='1234',
  126. )
  127. == ()
  128. )
  129. def test_filter_checks_on_frequency_restains_check_with_unelapsed_frequency_and_force():
  130. assert module.filter_checks_on_frequency(
  131. config={'checks': [{'name': 'archives', 'frequency': '1 hour'}]},
  132. borg_repository_id='repo',
  133. checks=('archives',),
  134. force=True,
  135. archives_check_id='1234',
  136. ) == ('archives',)
  137. def test_filter_checks_on_frequency_passes_through_empty_checks():
  138. assert (
  139. module.filter_checks_on_frequency(
  140. config={'checks': [{'name': 'archives', 'frequency': '1 hour'}]},
  141. borg_repository_id='repo',
  142. checks=(),
  143. force=False,
  144. archives_check_id='1234',
  145. )
  146. == ()
  147. )
  148. def test_make_archives_check_id_with_flags_returns_a_value_and_does_not_raise():
  149. assert module.make_archives_check_id(('--match-archives', 'sh:foo-*'))
  150. def test_make_archives_check_id_with_empty_flags_returns_none():
  151. assert module.make_archives_check_id(()) is None
  152. def test_make_check_time_path_with_borgmatic_source_directory_includes_it():
  153. flexmock(module.os.path).should_receive('expanduser').with_args('~/.borgmatic').and_return(
  154. '/home/user/.borgmatic'
  155. )
  156. assert (
  157. module.make_check_time_path(
  158. {'borgmatic_source_directory': '~/.borgmatic'}, '1234', 'archives', '5678'
  159. )
  160. == '/home/user/.borgmatic/checks/1234/archives/5678'
  161. )
  162. def test_make_check_time_path_without_borgmatic_source_directory_uses_default():
  163. flexmock(module.os.path).should_receive('expanduser').with_args(
  164. module.borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
  165. ).and_return('/home/user/.borgmatic')
  166. assert (
  167. module.make_check_time_path({}, '1234', 'archives', '5678')
  168. == '/home/user/.borgmatic/checks/1234/archives/5678'
  169. )
  170. def test_make_check_time_path_with_archives_check_and_no_archives_check_id_defaults_to_all():
  171. flexmock(module.os.path).should_receive('expanduser').with_args('~/.borgmatic').and_return(
  172. '/home/user/.borgmatic'
  173. )
  174. assert (
  175. module.make_check_time_path(
  176. {'borgmatic_source_directory': '~/.borgmatic'},
  177. '1234',
  178. 'archives',
  179. )
  180. == '/home/user/.borgmatic/checks/1234/archives/all'
  181. )
  182. def test_make_check_time_path_with_repositories_check_ignores_archives_check_id():
  183. flexmock(module.os.path).should_receive('expanduser').with_args('~/.borgmatic').and_return(
  184. '/home/user/.borgmatic'
  185. )
  186. assert (
  187. module.make_check_time_path(
  188. {'borgmatic_source_directory': '~/.borgmatic'}, '1234', 'repository', '5678'
  189. )
  190. == '/home/user/.borgmatic/checks/1234/repository'
  191. )
  192. def test_read_check_time_does_not_raise():
  193. flexmock(module.os).should_receive('stat').and_return(flexmock(st_mtime=123))
  194. assert module.read_check_time('/path')
  195. def test_read_check_time_on_missing_file_does_not_raise():
  196. flexmock(module.os).should_receive('stat').and_raise(FileNotFoundError)
  197. assert module.read_check_time('/path') is None
  198. def test_probe_for_check_time_uses_maximum_of_multiple_check_times():
  199. flexmock(module).should_receive('make_check_time_path').and_return(
  200. '~/.borgmatic/checks/1234/archives/5678'
  201. ).and_return('~/.borgmatic/checks/1234/archives/all')
  202. flexmock(module).should_receive('read_check_time').and_return(1).and_return(2)
  203. assert module.probe_for_check_time(flexmock(), flexmock(), flexmock(), flexmock()) == 2
  204. def test_probe_for_check_time_deduplicates_identical_check_time_paths():
  205. flexmock(module).should_receive('make_check_time_path').and_return(
  206. '~/.borgmatic/checks/1234/archives/5678'
  207. ).and_return('~/.borgmatic/checks/1234/archives/5678')
  208. flexmock(module).should_receive('read_check_time').and_return(1).once()
  209. assert module.probe_for_check_time(flexmock(), flexmock(), flexmock(), flexmock()) == 1
  210. def test_probe_for_check_time_skips_none_check_time():
  211. flexmock(module).should_receive('make_check_time_path').and_return(
  212. '~/.borgmatic/checks/1234/archives/5678'
  213. ).and_return('~/.borgmatic/checks/1234/archives/all')
  214. flexmock(module).should_receive('read_check_time').and_return(None).and_return(2)
  215. assert module.probe_for_check_time(flexmock(), flexmock(), flexmock(), flexmock()) == 2
  216. def test_probe_for_check_time_uses_single_check_time():
  217. flexmock(module).should_receive('make_check_time_path').and_return(
  218. '~/.borgmatic/checks/1234/archives/5678'
  219. ).and_return('~/.borgmatic/checks/1234/archives/all')
  220. flexmock(module).should_receive('read_check_time').and_return(1).and_return(None)
  221. assert module.probe_for_check_time(flexmock(), flexmock(), flexmock(), flexmock()) == 1
  222. def test_probe_for_check_time_returns_none_when_no_check_time_found():
  223. flexmock(module).should_receive('make_check_time_path').and_return(
  224. '~/.borgmatic/checks/1234/archives/5678'
  225. ).and_return('~/.borgmatic/checks/1234/archives/all')
  226. flexmock(module).should_receive('read_check_time').and_return(None).and_return(None)
  227. assert module.probe_for_check_time(flexmock(), flexmock(), flexmock(), flexmock()) is None
  228. def test_upgrade_check_times_renames_old_check_paths_to_all():
  229. base_path = '~/.borgmatic/checks/1234'
  230. flexmock(module).should_receive('make_check_time_path').with_args(
  231. object, object, 'archives', 'all'
  232. ).and_return(f'{base_path}/archives/all')
  233. flexmock(module).should_receive('make_check_time_path').with_args(
  234. object, object, 'data', 'all'
  235. ).and_return(f'{base_path}/data/all')
  236. flexmock(module.os.path).should_receive('isfile').with_args(f'{base_path}/archives').and_return(
  237. True
  238. )
  239. flexmock(module.os.path).should_receive('isfile').with_args(
  240. f'{base_path}/archives.temp'
  241. ).and_return(False)
  242. flexmock(module.os.path).should_receive('isfile').with_args(f'{base_path}/data').and_return(
  243. False
  244. )
  245. flexmock(module.os.path).should_receive('isfile').with_args(
  246. f'{base_path}/data.temp'
  247. ).and_return(False)
  248. flexmock(module.os).should_receive('rename').with_args(
  249. f'{base_path}/archives', f'{base_path}/archives.temp'
  250. ).once()
  251. flexmock(module.os).should_receive('mkdir').with_args(f'{base_path}/archives').once()
  252. flexmock(module.os).should_receive('rename').with_args(
  253. f'{base_path}/archives.temp', f'{base_path}/archives/all'
  254. ).once()
  255. module.upgrade_check_times(flexmock(), flexmock())
  256. def test_upgrade_check_times_renames_data_check_paths_when_archives_paths_are_already_upgraded():
  257. base_path = '~/.borgmatic/checks/1234'
  258. flexmock(module).should_receive('make_check_time_path').with_args(
  259. object, object, 'archives', 'all'
  260. ).and_return(f'{base_path}/archives/all')
  261. flexmock(module).should_receive('make_check_time_path').with_args(
  262. object, object, 'data', 'all'
  263. ).and_return(f'{base_path}/data/all')
  264. flexmock(module.os.path).should_receive('isfile').with_args(f'{base_path}/archives').and_return(
  265. False
  266. )
  267. flexmock(module.os.path).should_receive('isfile').with_args(
  268. f'{base_path}/archives.temp'
  269. ).and_return(False)
  270. flexmock(module.os.path).should_receive('isfile').with_args(f'{base_path}/data').and_return(
  271. True
  272. )
  273. flexmock(module.os).should_receive('rename').with_args(
  274. f'{base_path}/data', f'{base_path}/data.temp'
  275. ).once()
  276. flexmock(module.os).should_receive('mkdir').with_args(f'{base_path}/data').once()
  277. flexmock(module.os).should_receive('rename').with_args(
  278. f'{base_path}/data.temp', f'{base_path}/data/all'
  279. ).once()
  280. module.upgrade_check_times(flexmock(), flexmock())
  281. def test_upgrade_check_times_skips_missing_check_paths():
  282. flexmock(module).should_receive('make_check_time_path').and_return(
  283. '~/.borgmatic/checks/1234/archives/all'
  284. )
  285. flexmock(module.os.path).should_receive('isfile').and_return(False)
  286. flexmock(module.os).should_receive('rename').never()
  287. flexmock(module.os).should_receive('mkdir').never()
  288. module.upgrade_check_times(flexmock(), flexmock())
  289. def test_upgrade_check_times_renames_stale_temporary_check_path():
  290. base_path = '~/.borgmatic/checks/1234'
  291. flexmock(module).should_receive('make_check_time_path').with_args(
  292. object, object, 'archives', 'all'
  293. ).and_return(f'{base_path}/archives/all')
  294. flexmock(module).should_receive('make_check_time_path').with_args(
  295. object, object, 'data', 'all'
  296. ).and_return(f'{base_path}/data/all')
  297. flexmock(module.os.path).should_receive('isfile').with_args(f'{base_path}/archives').and_return(
  298. False
  299. )
  300. flexmock(module.os.path).should_receive('isfile').with_args(
  301. f'{base_path}/archives.temp'
  302. ).and_return(True)
  303. flexmock(module.os.path).should_receive('isfile').with_args(f'{base_path}/data').and_return(
  304. False
  305. )
  306. flexmock(module.os.path).should_receive('isfile').with_args(
  307. f'{base_path}/data.temp'
  308. ).and_return(False)
  309. flexmock(module.os).should_receive('rename').with_args(
  310. f'{base_path}/archives', f'{base_path}/archives.temp'
  311. ).and_raise(FileNotFoundError)
  312. flexmock(module.os).should_receive('mkdir').with_args(f'{base_path}/archives').once()
  313. flexmock(module.os).should_receive('rename').with_args(
  314. f'{base_path}/archives.temp', f'{base_path}/archives/all'
  315. ).once()
  316. module.upgrade_check_times(flexmock(), flexmock())
  317. def test_collect_spot_check_source_paths_parses_borg_output():
  318. flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
  319. {'hook1': False, 'hook2': True}
  320. )
  321. flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
  322. dry_run=True,
  323. repository_path='repo',
  324. config=object,
  325. config_paths=(),
  326. local_borg_version=object,
  327. global_arguments=object,
  328. borgmatic_source_directories=(),
  329. local_path=object,
  330. remote_path=object,
  331. list_files=True,
  332. stream_processes=True,
  333. ).and_return((('borg', 'create'), ('repo::archive',), flexmock(), flexmock()))
  334. flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
  335. flexmock()
  336. )
  337. flexmock(module.borgmatic.execute).should_receive(
  338. 'execute_command_and_capture_output'
  339. ).and_return(
  340. 'warning: stuff\n- /etc/path\n+ /etc/other\n? /nope',
  341. )
  342. flexmock(module.os.path).should_receive('isfile').and_return(True)
  343. assert module.collect_spot_check_source_paths(
  344. repository={'path': 'repo'},
  345. config={'working_directory': '/'},
  346. local_borg_version=flexmock(),
  347. global_arguments=flexmock(),
  348. local_path=flexmock(),
  349. remote_path=flexmock(),
  350. ) == ('/etc/path', '/etc/other')
  351. def test_collect_spot_check_source_paths_passes_through_stream_processes_false():
  352. flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
  353. {'hook1': False, 'hook2': False}
  354. )
  355. flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
  356. dry_run=True,
  357. repository_path='repo',
  358. config=object,
  359. config_paths=(),
  360. local_borg_version=object,
  361. global_arguments=object,
  362. borgmatic_source_directories=(),
  363. local_path=object,
  364. remote_path=object,
  365. list_files=True,
  366. stream_processes=False,
  367. ).and_return((('borg', 'create'), ('repo::archive',), flexmock(), flexmock()))
  368. flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
  369. flexmock()
  370. )
  371. flexmock(module.borgmatic.execute).should_receive(
  372. 'execute_command_and_capture_output'
  373. ).and_return(
  374. 'warning: stuff\n- /etc/path\n+ /etc/other\n? /nope',
  375. )
  376. flexmock(module.os.path).should_receive('isfile').and_return(True)
  377. assert module.collect_spot_check_source_paths(
  378. repository={'path': 'repo'},
  379. config={'working_directory': '/'},
  380. local_borg_version=flexmock(),
  381. global_arguments=flexmock(),
  382. local_path=flexmock(),
  383. remote_path=flexmock(),
  384. ) == ('/etc/path', '/etc/other')
  385. def test_collect_spot_check_source_paths_without_working_directory_parses_borg_output():
  386. flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
  387. {'hook1': False, 'hook2': True}
  388. )
  389. flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
  390. dry_run=True,
  391. repository_path='repo',
  392. config=object,
  393. config_paths=(),
  394. local_borg_version=object,
  395. global_arguments=object,
  396. borgmatic_source_directories=(),
  397. local_path=object,
  398. remote_path=object,
  399. list_files=True,
  400. stream_processes=True,
  401. ).and_return((('borg', 'create'), ('repo::archive',), flexmock(), flexmock()))
  402. flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
  403. flexmock()
  404. )
  405. flexmock(module.borgmatic.execute).should_receive(
  406. 'execute_command_and_capture_output'
  407. ).and_return(
  408. 'warning: stuff\n- /etc/path\n+ /etc/other\n? /nope',
  409. )
  410. flexmock(module.os.path).should_receive('isfile').and_return(True)
  411. assert module.collect_spot_check_source_paths(
  412. repository={'path': 'repo'},
  413. config={},
  414. local_borg_version=flexmock(),
  415. global_arguments=flexmock(),
  416. local_path=flexmock(),
  417. remote_path=flexmock(),
  418. ) == ('/etc/path', '/etc/other')
  419. def test_collect_spot_check_source_paths_includes_symlinks_but_skips_directories():
  420. flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
  421. {'hook1': False, 'hook2': True}
  422. )
  423. flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
  424. dry_run=True,
  425. repository_path='repo',
  426. config=object,
  427. config_paths=(),
  428. local_borg_version=object,
  429. global_arguments=object,
  430. borgmatic_source_directories=(),
  431. local_path=object,
  432. remote_path=object,
  433. list_files=True,
  434. stream_processes=True,
  435. ).and_return((('borg', 'create'), ('repo::archive',), flexmock(), flexmock()))
  436. flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
  437. flexmock()
  438. )
  439. flexmock(module.borgmatic.execute).should_receive(
  440. 'execute_command_and_capture_output'
  441. ).and_return(
  442. 'warning: stuff\n- /etc/path\n+ /etc/dir\n? /nope',
  443. )
  444. flexmock(module.os.path).should_receive('isfile').with_args('/etc/path').and_return(False)
  445. flexmock(module.os.path).should_receive('islink').with_args('/etc/path').and_return(True)
  446. flexmock(module.os.path).should_receive('isfile').with_args('/etc/dir').and_return(False)
  447. flexmock(module.os.path).should_receive('islink').with_args('/etc/dir').and_return(False)
  448. assert module.collect_spot_check_source_paths(
  449. repository={'path': 'repo'},
  450. config={'working_directory': '/'},
  451. local_borg_version=flexmock(),
  452. global_arguments=flexmock(),
  453. local_path=flexmock(),
  454. remote_path=flexmock(),
  455. ) == ('/etc/path',)
  456. def test_collect_spot_check_archive_paths_excludes_directories():
  457. flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
  458. (
  459. 'f /etc/path',
  460. 'f /etc/other',
  461. 'd /etc/dir',
  462. )
  463. )
  464. assert module.collect_spot_check_archive_paths(
  465. repository={'path': 'repo'},
  466. archive='archive',
  467. config={},
  468. local_borg_version=flexmock(),
  469. global_arguments=flexmock(),
  470. local_path=flexmock(),
  471. remote_path=flexmock(),
  472. ) == ('/etc/path', '/etc/other')
  473. def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_source_directory():
  474. flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
  475. (
  476. 'f /etc/path',
  477. 'f /root/.borgmatic/some/thing',
  478. )
  479. )
  480. assert module.collect_spot_check_archive_paths(
  481. repository={'path': 'repo'},
  482. archive='archive',
  483. config={'borgmatic_source_directory': '/root/.borgmatic'},
  484. local_borg_version=flexmock(),
  485. global_arguments=flexmock(),
  486. local_path=flexmock(),
  487. remote_path=flexmock(),
  488. ) == ('/etc/path',)
  489. def test_compare_spot_check_hashes_returns_paths_having_failing_hashes():
  490. flexmock(module.random).should_receive('sample').replace_with(
  491. lambda population, count: population[:count]
  492. )
  493. flexmock(module.os.path).should_receive('exists').and_return(True)
  494. flexmock(module.borgmatic.execute).should_receive(
  495. 'execute_command_and_capture_output'
  496. ).with_args(('xxh64sum', '/foo', '/bar')).and_return('hash1 /foo\nhash2 /bar')
  497. flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
  498. ['hash1 /foo', 'nothash2 /bar']
  499. )
  500. assert module.compare_spot_check_hashes(
  501. repository={'path': 'repo'},
  502. archive='archive',
  503. config={
  504. 'checks': [
  505. {
  506. 'name': 'archives',
  507. 'frequency': '2 weeks',
  508. },
  509. {
  510. 'name': 'spot',
  511. 'data_sample_percentage': 50,
  512. },
  513. ]
  514. },
  515. local_borg_version=flexmock(),
  516. global_arguments=flexmock(),
  517. local_path=flexmock(),
  518. remote_path=flexmock(),
  519. log_label='repo',
  520. source_paths=('/foo', '/bar', '/baz', '/quux'),
  521. ) == ('/bar',)
  522. def test_compare_spot_check_hashes_handles_data_sample_percentage_above_100():
  523. flexmock(module.random).should_receive('sample').replace_with(
  524. lambda population, count: population[:count]
  525. )
  526. flexmock(module.os.path).should_receive('exists').and_return(True)
  527. flexmock(module.borgmatic.execute).should_receive(
  528. 'execute_command_and_capture_output'
  529. ).with_args(('xxh64sum', '/foo', '/bar')).and_return('hash1 /foo\nhash2 /bar')
  530. flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
  531. ['nothash1 /foo', 'nothash2 /bar']
  532. )
  533. assert module.compare_spot_check_hashes(
  534. repository={'path': 'repo'},
  535. archive='archive',
  536. config={
  537. 'checks': [
  538. {
  539. 'name': 'archives',
  540. 'frequency': '2 weeks',
  541. },
  542. {
  543. 'name': 'spot',
  544. 'data_sample_percentage': 1000,
  545. },
  546. ]
  547. },
  548. local_borg_version=flexmock(),
  549. global_arguments=flexmock(),
  550. local_path=flexmock(),
  551. remote_path=flexmock(),
  552. log_label='repo',
  553. source_paths=('/foo', '/bar'),
  554. ) == ('/foo', '/bar')
  555. def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
  556. flexmock(module.random).should_receive('sample').replace_with(
  557. lambda population, count: population[:count]
  558. )
  559. flexmock(module.os.path).should_receive('exists').and_return(True)
  560. flexmock(module.borgmatic.execute).should_receive(
  561. 'execute_command_and_capture_output'
  562. ).with_args(('/usr/local/bin/xxh64sum', '/foo', '/bar')).and_return('hash1 /foo\nhash2 /bar')
  563. flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
  564. ['hash1 /foo', 'nothash2 /bar']
  565. )
  566. assert module.compare_spot_check_hashes(
  567. repository={'path': 'repo'},
  568. archive='archive',
  569. config={
  570. 'checks': [
  571. {
  572. 'name': 'spot',
  573. 'data_sample_percentage': 50,
  574. 'xxh64sum_command': '/usr/local/bin/xxh64sum',
  575. },
  576. ]
  577. },
  578. local_borg_version=flexmock(),
  579. global_arguments=flexmock(),
  580. local_path=flexmock(),
  581. remote_path=flexmock(),
  582. log_label='repo',
  583. source_paths=('/foo', '/bar', '/baz', '/quux'),
  584. ) == ('/bar',)
  585. def test_compare_spot_check_hashes_consider_path_missing_from_archive_as_not_matching():
  586. flexmock(module.random).should_receive('sample').replace_with(
  587. lambda population, count: population[:count]
  588. )
  589. flexmock(module.os.path).should_receive('exists').and_return(True)
  590. flexmock(module.borgmatic.execute).should_receive(
  591. 'execute_command_and_capture_output'
  592. ).with_args(('xxh64sum', '/foo', '/bar')).and_return('hash1 /foo\nhash2 /bar')
  593. flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
  594. ['hash1 /foo']
  595. )
  596. assert module.compare_spot_check_hashes(
  597. repository={'path': 'repo'},
  598. archive='archive',
  599. config={
  600. 'checks': [
  601. {
  602. 'name': 'spot',
  603. 'data_sample_percentage': 50,
  604. },
  605. ]
  606. },
  607. local_borg_version=flexmock(),
  608. global_arguments=flexmock(),
  609. local_path=flexmock(),
  610. remote_path=flexmock(),
  611. log_label='repo',
  612. source_paths=('/foo', '/bar', '/baz', '/quux'),
  613. ) == ('/bar',)
  614. def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching():
  615. flexmock(module.random).should_receive('sample').replace_with(
  616. lambda population, count: population[:count]
  617. )
  618. flexmock(module.os.path).should_receive('exists').with_args('/foo').and_return(True)
  619. flexmock(module.os.path).should_receive('exists').with_args('/bar').and_return(False)
  620. flexmock(module.borgmatic.execute).should_receive(
  621. 'execute_command_and_capture_output'
  622. ).with_args(('xxh64sum', '/foo')).and_return('hash1 /foo')
  623. flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
  624. ['hash1 /foo', 'hash2 /bar']
  625. )
  626. assert module.compare_spot_check_hashes(
  627. repository={'path': 'repo'},
  628. archive='archive',
  629. config={
  630. 'checks': [
  631. {
  632. 'name': 'spot',
  633. 'data_sample_percentage': 50,
  634. },
  635. ]
  636. },
  637. local_borg_version=flexmock(),
  638. global_arguments=flexmock(),
  639. local_path=flexmock(),
  640. remote_path=flexmock(),
  641. log_label='repo',
  642. source_paths=('/foo', '/bar', '/baz', '/quux'),
  643. ) == ('/bar',)
  644. def test_spot_check_without_spot_configuration_errors():
  645. with pytest.raises(ValueError):
  646. module.spot_check(
  647. repository={'path': 'repo'},
  648. config={
  649. 'checks': [
  650. {
  651. 'name': 'archives',
  652. },
  653. ]
  654. },
  655. local_borg_version=flexmock(),
  656. global_arguments=flexmock(),
  657. local_path=flexmock(),
  658. remote_path=flexmock(),
  659. )
  660. def test_spot_check_without_any_configuration_errors():
  661. with pytest.raises(ValueError):
  662. module.spot_check(
  663. repository={'path': 'repo'},
  664. config={},
  665. local_borg_version=flexmock(),
  666. global_arguments=flexmock(),
  667. local_path=flexmock(),
  668. remote_path=flexmock(),
  669. )
  670. def test_spot_check_data_tolerance_percenatge_greater_than_data_sample_percentage_errors():
  671. with pytest.raises(ValueError):
  672. module.spot_check(
  673. repository={'path': 'repo'},
  674. config={
  675. 'checks': [
  676. {
  677. 'name': 'spot',
  678. 'data_tolerance_percentage': 7,
  679. 'data_sample_percentage': 5,
  680. },
  681. ]
  682. },
  683. local_borg_version=flexmock(),
  684. global_arguments=flexmock(),
  685. local_path=flexmock(),
  686. remote_path=flexmock(),
  687. )
  688. def test_spot_check_with_count_delta_greater_than_count_tolerance_percentage_errors():
  689. flexmock(module).should_receive('collect_spot_check_source_paths').and_return(
  690. ('/foo', '/bar', '/baz', '/quux')
  691. )
  692. flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
  693. 'archive'
  694. )
  695. flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(
  696. ('/foo', '/bar')
  697. ).once()
  698. with pytest.raises(ValueError):
  699. module.spot_check(
  700. repository={'path': 'repo'},
  701. config={
  702. 'checks': [
  703. {
  704. 'name': 'spot',
  705. 'count_tolerance_percentage': 1,
  706. 'data_tolerance_percentage': 4,
  707. 'data_sample_percentage': 5,
  708. },
  709. ]
  710. },
  711. local_borg_version=flexmock(),
  712. global_arguments=flexmock(),
  713. local_path=flexmock(),
  714. remote_path=flexmock(),
  715. )
  716. def test_spot_check_with_failing_percentage_greater_than_data_tolerance_percentage_errors():
  717. flexmock(module).should_receive('collect_spot_check_source_paths').and_return(
  718. ('/foo', '/bar', '/baz', '/quux')
  719. )
  720. flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
  721. 'archive'
  722. )
  723. flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(('/foo', '/bar'))
  724. flexmock(module).should_receive('compare_spot_check_hashes').and_return(
  725. ('/bar', '/baz', '/quux')
  726. ).once()
  727. with pytest.raises(ValueError):
  728. module.spot_check(
  729. repository={'path': 'repo'},
  730. config={
  731. 'checks': [
  732. {
  733. 'name': 'spot',
  734. 'count_tolerance_percentage': 55,
  735. 'data_tolerance_percentage': 4,
  736. 'data_sample_percentage': 5,
  737. },
  738. ]
  739. },
  740. local_borg_version=flexmock(),
  741. global_arguments=flexmock(),
  742. local_path=flexmock(),
  743. remote_path=flexmock(),
  744. )
  745. def test_spot_check_with_high_enough_tolerances_does_not_raise():
  746. flexmock(module).should_receive('collect_spot_check_source_paths').and_return(
  747. ('/foo', '/bar', '/baz', '/quux')
  748. )
  749. flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
  750. 'archive'
  751. )
  752. flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(('/foo', '/bar'))
  753. flexmock(module).should_receive('compare_spot_check_hashes').and_return(
  754. ('/bar', '/baz', '/quux')
  755. ).once()
  756. module.spot_check(
  757. repository={'path': 'repo'},
  758. config={
  759. 'checks': [
  760. {
  761. 'name': 'spot',
  762. 'count_tolerance_percentage': 55,
  763. 'data_tolerance_percentage': 80,
  764. 'data_sample_percentage': 80,
  765. },
  766. ]
  767. },
  768. local_borg_version=flexmock(),
  769. global_arguments=flexmock(),
  770. local_path=flexmock(),
  771. remote_path=flexmock(),
  772. )
  773. def test_run_check_checks_archives_for_configured_repository():
  774. flexmock(module.logger).answer = lambda message: None
  775. flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
  776. flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
  777. flexmock(module).should_receive('upgrade_check_times')
  778. flexmock(module).should_receive('parse_checks')
  779. flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
  780. flexmock(module).should_receive('make_archives_check_id').and_return(None)
  781. flexmock(module).should_receive('filter_checks_on_frequency').and_return(
  782. {'repository', 'archives'}
  783. )
  784. flexmock(module.borgmatic.borg.check).should_receive('check_archives').once()
  785. flexmock(module).should_receive('make_check_time_path')
  786. flexmock(module).should_receive('write_check_time')
  787. flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
  788. flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
  789. check_arguments = flexmock(
  790. repository=None,
  791. progress=flexmock(),
  792. repair=flexmock(),
  793. only_checks=flexmock(),
  794. force=flexmock(),
  795. )
  796. global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
  797. module.run_check(
  798. config_filename='test.yaml',
  799. repository={'path': 'repo'},
  800. config={'repositories': ['repo']},
  801. hook_context={},
  802. local_borg_version=None,
  803. check_arguments=check_arguments,
  804. global_arguments=global_arguments,
  805. local_path=None,
  806. remote_path=None,
  807. )
  808. def test_run_check_runs_configured_extract_check():
  809. flexmock(module.logger).answer = lambda message: None
  810. flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
  811. flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
  812. flexmock(module).should_receive('upgrade_check_times')
  813. flexmock(module).should_receive('parse_checks')
  814. flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
  815. flexmock(module).should_receive('make_archives_check_id').and_return(None)
  816. flexmock(module).should_receive('filter_checks_on_frequency').and_return({'extract'})
  817. flexmock(module.borgmatic.borg.check).should_receive('check_archives').never()
  818. flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').once()
  819. flexmock(module).should_receive('make_check_time_path')
  820. flexmock(module).should_receive('write_check_time')
  821. flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
  822. check_arguments = flexmock(
  823. repository=None,
  824. progress=flexmock(),
  825. repair=flexmock(),
  826. only_checks=flexmock(),
  827. force=flexmock(),
  828. )
  829. global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
  830. module.run_check(
  831. config_filename='test.yaml',
  832. repository={'path': 'repo'},
  833. config={'repositories': ['repo']},
  834. hook_context={},
  835. local_borg_version=None,
  836. check_arguments=check_arguments,
  837. global_arguments=global_arguments,
  838. local_path=None,
  839. remote_path=None,
  840. )
  841. def test_run_check_runs_configured_spot_check():
  842. flexmock(module.logger).answer = lambda message: None
  843. flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
  844. flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
  845. flexmock(module).should_receive('upgrade_check_times')
  846. flexmock(module).should_receive('parse_checks')
  847. flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
  848. flexmock(module).should_receive('make_archives_check_id').and_return(None)
  849. flexmock(module).should_receive('filter_checks_on_frequency').and_return({'spot'})
  850. flexmock(module.borgmatic.borg.check).should_receive('check_archives').never()
  851. flexmock(module.borgmatic.actions.check).should_receive('spot_check').once()
  852. flexmock(module).should_receive('make_check_time_path')
  853. flexmock(module).should_receive('write_check_time')
  854. flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
  855. check_arguments = flexmock(
  856. repository=None,
  857. progress=flexmock(),
  858. repair=flexmock(),
  859. only_checks=flexmock(),
  860. force=flexmock(),
  861. )
  862. global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
  863. module.run_check(
  864. config_filename='test.yaml',
  865. repository={'path': 'repo'},
  866. config={'repositories': ['repo']},
  867. hook_context={},
  868. local_borg_version=None,
  869. check_arguments=check_arguments,
  870. global_arguments=global_arguments,
  871. local_path=None,
  872. remote_path=None,
  873. )
  874. def test_run_check_without_checks_runs_nothing_except_hooks():
  875. flexmock(module.logger).answer = lambda message: None
  876. flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
  877. flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
  878. flexmock(module).should_receive('upgrade_check_times')
  879. flexmock(module).should_receive('parse_checks')
  880. flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
  881. flexmock(module).should_receive('make_archives_check_id').and_return(None)
  882. flexmock(module).should_receive('filter_checks_on_frequency').and_return({})
  883. flexmock(module.borgmatic.borg.check).should_receive('check_archives').never()
  884. flexmock(module).should_receive('make_check_time_path')
  885. flexmock(module).should_receive('write_check_time').never()
  886. flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
  887. flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
  888. check_arguments = flexmock(
  889. repository=None,
  890. progress=flexmock(),
  891. repair=flexmock(),
  892. only_checks=flexmock(),
  893. force=flexmock(),
  894. )
  895. global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
  896. module.run_check(
  897. config_filename='test.yaml',
  898. repository={'path': 'repo'},
  899. config={'repositories': ['repo']},
  900. hook_context={},
  901. local_borg_version=None,
  902. check_arguments=check_arguments,
  903. global_arguments=global_arguments,
  904. local_path=None,
  905. remote_path=None,
  906. )
  907. def test_run_check_checks_archives_in_selected_repository():
  908. flexmock(module.logger).answer = lambda message: None
  909. flexmock(module.borgmatic.config.validate).should_receive(
  910. 'repositories_match'
  911. ).once().and_return(True)
  912. flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
  913. flexmock(module).should_receive('upgrade_check_times')
  914. flexmock(module).should_receive('parse_checks')
  915. flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
  916. flexmock(module).should_receive('make_archives_check_id').and_return(None)
  917. flexmock(module).should_receive('filter_checks_on_frequency').and_return(
  918. {'repository', 'archives'}
  919. )
  920. flexmock(module.borgmatic.borg.check).should_receive('check_archives').once()
  921. flexmock(module).should_receive('make_check_time_path')
  922. flexmock(module).should_receive('write_check_time')
  923. flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
  924. check_arguments = flexmock(
  925. repository=flexmock(),
  926. progress=flexmock(),
  927. repair=flexmock(),
  928. only_checks=flexmock(),
  929. force=flexmock(),
  930. )
  931. global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
  932. module.run_check(
  933. config_filename='test.yaml',
  934. repository={'path': 'repo'},
  935. config={'repositories': ['repo']},
  936. hook_context={},
  937. local_borg_version=None,
  938. check_arguments=check_arguments,
  939. global_arguments=global_arguments,
  940. local_path=None,
  941. remote_path=None,
  942. )
  943. def test_run_check_bails_if_repository_does_not_match():
  944. flexmock(module.logger).answer = lambda message: None
  945. flexmock(module.borgmatic.config.validate).should_receive(
  946. 'repositories_match'
  947. ).once().and_return(False)
  948. flexmock(module.borgmatic.borg.check).should_receive('check_archives').never()
  949. check_arguments = flexmock(
  950. repository=flexmock(),
  951. progress=flexmock(),
  952. repair=flexmock(),
  953. only_checks=flexmock(),
  954. force=flexmock(),
  955. )
  956. global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
  957. module.run_check(
  958. config_filename='test.yaml',
  959. repository={'path': 'repo'},
  960. config={'repositories': ['repo']},
  961. hook_context={},
  962. local_borg_version=None,
  963. check_arguments=check_arguments,
  964. global_arguments=global_arguments,
  965. local_path=None,
  966. remote_path=None,
  967. )