test_command.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import logging
  2. import subprocess
  3. import pytest
  4. from flexmock import flexmock
  5. from borgmatic.hooks import command as module
  6. def test_interpolate_context_passes_through_command_without_variable():
  7. assert module.interpolate_context('pre-backup', 'ls', {'foo': 'bar'}) == 'ls'
  8. def test_interpolate_context_passes_through_command_with_unknown_variable():
  9. command = 'ls {baz}' # noqa: FS003
  10. assert module.interpolate_context('pre-backup', command, {'foo': 'bar'}) == command
  11. def test_interpolate_context_interpolates_variables():
  12. command = 'ls {foo}{baz} {baz}' # noqa: FS003
  13. context = {'foo': 'bar', 'baz': 'quux'}
  14. assert module.interpolate_context('pre-backup', command, context) == 'ls barquux quux'
  15. def test_interpolate_context_escapes_interpolated_variables():
  16. command = 'ls {foo} {inject}' # noqa: FS003
  17. context = {'foo': 'bar', 'inject': 'hi; naughty-command'}
  18. assert (
  19. module.interpolate_context('pre-backup', command, context) == "ls bar 'hi; naughty-command'"
  20. )
  21. def test_make_environment_without_pyinstaller_does_not_touch_environment():
  22. assert module.make_environment({}, sys_module=flexmock()) == {}
  23. def test_make_environment_with_pyinstaller_clears_LD_LIBRARY_PATH():
  24. assert module.make_environment({}, sys_module=flexmock(frozen=True, _MEIPASS='yup')) == {
  25. 'LD_LIBRARY_PATH': ''
  26. }
  27. def test_make_environment_with_pyinstaller_and_LD_LIBRARY_PATH_ORIG_copies_it_into_LD_LIBRARY_PATH():
  28. assert module.make_environment(
  29. {'LD_LIBRARY_PATH_ORIG': '/lib/lib/lib'}, sys_module=flexmock(frozen=True, _MEIPASS='yup')
  30. ) == {'LD_LIBRARY_PATH_ORIG': '/lib/lib/lib', 'LD_LIBRARY_PATH': '/lib/lib/lib'}
  31. @pytest.mark.parametrize(
  32. 'hooks,filters,expected_hooks',
  33. (
  34. (
  35. (
  36. {
  37. 'before': 'action',
  38. 'run': ['foo'],
  39. },
  40. {
  41. 'after': 'action',
  42. 'run': ['bar'],
  43. },
  44. {
  45. 'before': 'repository',
  46. 'run': ['baz'],
  47. },
  48. ),
  49. {},
  50. (
  51. {
  52. 'before': 'action',
  53. 'run': ['foo'],
  54. },
  55. {
  56. 'after': 'action',
  57. 'run': ['bar'],
  58. },
  59. {
  60. 'before': 'repository',
  61. 'run': ['baz'],
  62. },
  63. ),
  64. ),
  65. (
  66. (
  67. {
  68. 'before': 'action',
  69. 'run': ['foo'],
  70. },
  71. {
  72. 'after': 'action',
  73. 'run': ['bar'],
  74. },
  75. {
  76. 'before': 'repository',
  77. 'run': ['baz'],
  78. },
  79. ),
  80. {
  81. 'before': 'action',
  82. },
  83. (
  84. {
  85. 'before': 'action',
  86. 'run': ['foo'],
  87. },
  88. ),
  89. ),
  90. (
  91. (
  92. {
  93. 'after': 'action',
  94. 'run': ['foo'],
  95. },
  96. {
  97. 'before': 'action',
  98. 'run': ['bar'],
  99. },
  100. {
  101. 'after': 'repository',
  102. 'run': ['baz'],
  103. },
  104. ),
  105. {
  106. 'after': 'action',
  107. },
  108. (
  109. {
  110. 'after': 'action',
  111. 'run': ['foo'],
  112. },
  113. ),
  114. ),
  115. (
  116. (
  117. {
  118. 'before': 'dump_data_sources',
  119. 'hooks': ['postgresql'],
  120. 'run': ['foo'],
  121. },
  122. {
  123. 'before': 'dump_data_sources',
  124. 'hooks': ['lvm'],
  125. 'run': ['bar'],
  126. },
  127. {
  128. 'after': 'dump_data_sources',
  129. 'hooks': ['lvm'],
  130. 'run': ['baz'],
  131. },
  132. ),
  133. {
  134. 'before': 'dump_data_sources',
  135. 'hook_name': 'lvm',
  136. },
  137. (
  138. {
  139. 'before': 'dump_data_sources',
  140. 'hooks': ['lvm'],
  141. 'run': ['bar'],
  142. },
  143. ),
  144. ),
  145. (
  146. (
  147. {
  148. 'before': 'dump_data_sources',
  149. 'run': ['foo'],
  150. },
  151. {
  152. 'before': 'dump_data_sources',
  153. 'run': ['bar'],
  154. },
  155. {
  156. 'after': 'dump_data_sources',
  157. 'run': ['baz'],
  158. },
  159. ),
  160. {
  161. 'before': 'dump_data_sources',
  162. 'hook_name': 'lvm',
  163. },
  164. (
  165. {
  166. 'before': 'dump_data_sources',
  167. 'run': ['foo'],
  168. },
  169. {
  170. 'before': 'dump_data_sources',
  171. 'run': ['bar'],
  172. },
  173. ),
  174. ),
  175. (
  176. (
  177. {
  178. 'before': 'dump_data_sources',
  179. 'hooks': ['postgresql', 'zfs', 'lvm'],
  180. 'run': ['foo'],
  181. },
  182. ),
  183. {
  184. 'before': 'dump_data_sources',
  185. 'hook_name': 'lvm',
  186. },
  187. (
  188. {
  189. 'before': 'dump_data_sources',
  190. 'hooks': ['postgresql', 'zfs', 'lvm'],
  191. 'run': ['foo'],
  192. },
  193. ),
  194. ),
  195. (
  196. (
  197. {
  198. 'before': 'action',
  199. 'when': ['create'],
  200. 'run': ['foo'],
  201. },
  202. {
  203. 'before': 'action',
  204. 'when': ['prune'],
  205. 'run': ['bar'],
  206. },
  207. {
  208. 'before': 'action',
  209. 'when': ['compact'],
  210. 'run': ['baz'],
  211. },
  212. ),
  213. {
  214. 'before': 'action',
  215. 'action_names': ['create', 'compact', 'extract'],
  216. },
  217. (
  218. {
  219. 'before': 'action',
  220. 'when': ['create'],
  221. 'run': ['foo'],
  222. },
  223. {
  224. 'before': 'action',
  225. 'when': ['compact'],
  226. 'run': ['baz'],
  227. },
  228. ),
  229. ),
  230. (
  231. (
  232. {
  233. 'before': 'action',
  234. 'run': ['foo'],
  235. },
  236. {
  237. 'before': 'action',
  238. 'run': ['bar'],
  239. },
  240. {
  241. 'before': 'action',
  242. 'run': ['baz'],
  243. },
  244. ),
  245. {
  246. 'before': 'action',
  247. 'action_names': ['create', 'compact', 'extract'],
  248. },
  249. (
  250. {
  251. 'before': 'action',
  252. 'run': ['foo'],
  253. },
  254. {
  255. 'before': 'action',
  256. 'run': ['bar'],
  257. },
  258. {
  259. 'before': 'action',
  260. 'run': ['baz'],
  261. },
  262. ),
  263. ),
  264. ),
  265. )
  266. def test_filter_hooks(hooks, filters, expected_hooks):
  267. assert module.filter_hooks(hooks, **filters) == expected_hooks
  268. LOGGING_ANSWER = flexmock()
  269. def test_execute_hooks_invokes_each_hook_and_command():
  270. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  271. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  272. flexmock(module).should_receive('interpolate_context').replace_with(
  273. lambda hook_description, command, context: command
  274. )
  275. flexmock(module).should_receive('make_environment').and_return({})
  276. for command in ('foo', 'bar', 'baz'):
  277. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  278. [command],
  279. output_log_level=LOGGING_ANSWER,
  280. shell=True,
  281. environment={},
  282. ).once()
  283. module.execute_hooks(
  284. [{'before': 'create', 'run': ['foo']}, {'before': 'create', 'run': ['bar', 'baz']}],
  285. umask=None,
  286. dry_run=False,
  287. )
  288. def test_execute_hooks_with_umask_sets_that_umask():
  289. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  290. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  291. flexmock(module).should_receive('interpolate_context').replace_with(
  292. lambda hook_description, command, context: command
  293. )
  294. flexmock(module.os).should_receive('umask').with_args(0o77).and_return(0o22).once()
  295. flexmock(module.os).should_receive('umask').with_args(0o22).once()
  296. flexmock(module).should_receive('make_environment').and_return({})
  297. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  298. ['foo'],
  299. output_log_level=logging.ANSWER,
  300. shell=True,
  301. environment={},
  302. )
  303. module.execute_hooks([{'before': 'create', 'run': ['foo']}], umask=77, dry_run=False)
  304. def test_execute_hooks_with_dry_run_skips_commands():
  305. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  306. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  307. flexmock(module).should_receive('interpolate_context').replace_with(
  308. lambda hook_description, command, context: command
  309. )
  310. flexmock(module).should_receive('make_environment').and_return({})
  311. flexmock(module.borgmatic.execute).should_receive('execute_command').never()
  312. module.execute_hooks([{'before': 'create', 'run': ['foo']}], umask=None, dry_run=True)
  313. def test_execute_hooks_with_empty_commands_does_not_raise():
  314. module.execute_hooks([], umask=None, dry_run=True)
  315. def test_execute_hooks_with_error_logs_as_error():
  316. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  317. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  318. flexmock(module).should_receive('interpolate_context').replace_with(
  319. lambda hook_description, command, context: command
  320. )
  321. flexmock(module).should_receive('make_environment').and_return({})
  322. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  323. ['foo'],
  324. output_log_level=logging.ERROR,
  325. shell=True,
  326. environment={},
  327. ).once()
  328. module.execute_hooks([{'after': 'error', 'run': ['foo']}], umask=None, dry_run=False)
  329. def test_execute_hooks_with_before_or_after_raises():
  330. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  331. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  332. flexmock(module).should_receive('interpolate_context').never()
  333. flexmock(module).should_receive('make_environment').never()
  334. flexmock(module.borgmatic.execute).should_receive('execute_command').never()
  335. with pytest.raises(ValueError):
  336. module.execute_hooks(
  337. [
  338. {'erstwhile': 'create', 'run': ['foo']},
  339. {'erstwhile': 'create', 'run': ['bar', 'baz']},
  340. ],
  341. umask=None,
  342. dry_run=False,
  343. )
  344. def test_execute_hooks_without_commands_to_run_does_not_raise():
  345. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  346. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  347. flexmock(module).should_receive('interpolate_context').replace_with(
  348. lambda hook_description, command, context: command
  349. )
  350. flexmock(module).should_receive('make_environment').and_return({})
  351. for command in ('foo', 'bar'):
  352. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  353. [command],
  354. output_log_level=LOGGING_ANSWER,
  355. shell=True,
  356. environment={},
  357. ).once()
  358. module.execute_hooks(
  359. [{'before': 'create', 'run': []}, {'before': 'create', 'run': ['foo', 'bar']}],
  360. umask=None,
  361. dry_run=False,
  362. )
  363. def test_before_after_hooks_calls_command_hooks():
  364. commands = [
  365. {'before': 'repository', 'run': ['foo', 'bar']},
  366. {'after': 'repository', 'run': ['baz']},
  367. ]
  368. flexmock(module).should_receive('filter_hooks').with_args(
  369. commands,
  370. before='action',
  371. hook_name='myhook',
  372. action_names=['create'],
  373. ).and_return(flexmock()).once()
  374. flexmock(module).should_receive('filter_hooks').with_args(
  375. commands,
  376. after='action',
  377. hook_name='myhook',
  378. action_names=['create'],
  379. ).and_return(flexmock()).once()
  380. flexmock(module).should_receive('execute_hooks').twice()
  381. with module.Before_after_hooks(
  382. command_hooks=commands,
  383. before_after='action',
  384. umask=1234,
  385. dry_run=False,
  386. hook_name='myhook',
  387. action_names=['create'],
  388. context1='stuff',
  389. context2='such',
  390. ):
  391. pass
  392. def test_considered_soft_failure_treats_soft_fail_exit_code_as_soft_fail():
  393. error = subprocess.CalledProcessError(module.SOFT_FAIL_EXIT_CODE, 'try again')
  394. assert module.considered_soft_failure(error)
  395. def test_considered_soft_failure_does_not_treat_other_exit_code_as_soft_fail():
  396. error = subprocess.CalledProcessError(1, 'error')
  397. assert not module.considered_soft_failure(error)
  398. def test_considered_soft_failure_does_not_treat_other_exception_type_as_soft_fail():
  399. assert not module.considered_soft_failure(Exception())