test_command.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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': 'action',
  119. 'run': ['foo'],
  120. },
  121. {
  122. 'before': 'action',
  123. 'run': ['bar'],
  124. },
  125. {
  126. 'before': 'action',
  127. 'run': ['baz'],
  128. },
  129. ),
  130. {
  131. 'before': 'action',
  132. 'action_names': ['create', 'compact', 'extract'],
  133. },
  134. (
  135. {
  136. 'before': 'action',
  137. 'run': ['foo'],
  138. },
  139. {
  140. 'before': 'action',
  141. 'run': ['bar'],
  142. },
  143. {
  144. 'before': 'action',
  145. 'run': ['baz'],
  146. },
  147. ),
  148. ),
  149. ),
  150. )
  151. def test_filter_hooks(hooks, filters, expected_hooks):
  152. assert module.filter_hooks(hooks, **filters) == expected_hooks
  153. LOGGING_ANSWER = flexmock()
  154. def test_execute_hooks_invokes_each_hook_and_command():
  155. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  156. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  157. flexmock(module).should_receive('interpolate_context').replace_with(
  158. lambda hook_description, command, context: command
  159. )
  160. flexmock(module).should_receive('make_environment').and_return({})
  161. for command in ('foo', 'bar', 'baz'):
  162. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  163. [command],
  164. output_log_level=LOGGING_ANSWER,
  165. shell=True,
  166. environment={},
  167. ).once()
  168. module.execute_hooks(
  169. [{'before': 'create', 'run': ['foo']}, {'before': 'create', 'run': ['bar', 'baz']}],
  170. umask=None,
  171. dry_run=False,
  172. )
  173. def test_execute_hooks_with_umask_sets_that_umask():
  174. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  175. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  176. flexmock(module).should_receive('interpolate_context').replace_with(
  177. lambda hook_description, command, context: command
  178. )
  179. flexmock(module.os).should_receive('umask').with_args(0o77).and_return(0o22).once()
  180. flexmock(module.os).should_receive('umask').with_args(0o22).once()
  181. flexmock(module).should_receive('make_environment').and_return({})
  182. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  183. ['foo'],
  184. output_log_level=logging.ANSWER,
  185. shell=True,
  186. environment={},
  187. )
  188. module.execute_hooks([{'before': 'create', 'run': ['foo']}], umask=77, dry_run=False)
  189. def test_execute_hooks_with_dry_run_skips_commands():
  190. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  191. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  192. flexmock(module).should_receive('interpolate_context').replace_with(
  193. lambda hook_description, command, context: command
  194. )
  195. flexmock(module).should_receive('make_environment').and_return({})
  196. flexmock(module.borgmatic.execute).should_receive('execute_command').never()
  197. module.execute_hooks([{'before': 'create', 'run': ['foo']}], umask=None, dry_run=True)
  198. def test_execute_hooks_with_empty_commands_does_not_raise():
  199. module.execute_hooks([], umask=None, dry_run=True)
  200. def test_execute_hooks_with_error_logs_as_error():
  201. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  202. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  203. flexmock(module).should_receive('interpolate_context').replace_with(
  204. lambda hook_description, command, context: command
  205. )
  206. flexmock(module).should_receive('make_environment').and_return({})
  207. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  208. ['foo'],
  209. output_log_level=logging.ERROR,
  210. shell=True,
  211. environment={},
  212. ).once()
  213. module.execute_hooks([{'after': 'error', 'run': ['foo']}], umask=None, dry_run=False)
  214. def test_execute_hooks_with_before_or_after_raises():
  215. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  216. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  217. flexmock(module).should_receive('interpolate_context').never()
  218. flexmock(module).should_receive('make_environment').never()
  219. flexmock(module.borgmatic.execute).should_receive('execute_command').never()
  220. with pytest.raises(ValueError):
  221. module.execute_hooks(
  222. [
  223. {'erstwhile': 'create', 'run': ['foo']},
  224. {'erstwhile': 'create', 'run': ['bar', 'baz']},
  225. ],
  226. umask=None,
  227. dry_run=False,
  228. )
  229. def test_execute_hooks_without_commands_to_run_does_not_raise():
  230. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  231. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  232. flexmock(module).should_receive('interpolate_context').replace_with(
  233. lambda hook_description, command, context: command
  234. )
  235. flexmock(module).should_receive('make_environment').and_return({})
  236. for command in ('foo', 'bar'):
  237. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  238. [command],
  239. output_log_level=LOGGING_ANSWER,
  240. shell=True,
  241. environment={},
  242. ).once()
  243. module.execute_hooks(
  244. [{'before': 'create', 'run': []}, {'before': 'create', 'run': ['foo', 'bar']}],
  245. umask=None,
  246. dry_run=False,
  247. )
  248. def test_before_after_hooks_calls_command_hooks():
  249. commands = [
  250. {'before': 'repository', 'run': ['foo', 'bar']},
  251. {'after': 'repository', 'run': ['baz']},
  252. ]
  253. flexmock(module).should_receive('filter_hooks').with_args(
  254. commands,
  255. before='action',
  256. hook_name='myhook',
  257. action_names=['create'],
  258. ).and_return(flexmock()).once()
  259. flexmock(module).should_receive('filter_hooks').with_args(
  260. commands,
  261. after='action',
  262. hook_name='myhook',
  263. action_names=['create'],
  264. ).and_return(flexmock()).once()
  265. flexmock(module).should_receive('execute_hooks').twice()
  266. with module.Before_after_hooks(
  267. command_hooks=commands,
  268. before_after='action',
  269. umask=1234,
  270. dry_run=False,
  271. hook_name='myhook',
  272. action_names=['create'],
  273. context1='stuff',
  274. context2='such',
  275. ):
  276. pass
  277. def test_before_after_hooks_with_before_error_runs_after_hook_and_raises():
  278. commands = [
  279. {'before': 'repository', 'run': ['foo', 'bar']},
  280. {'after': 'repository', 'run': ['baz']},
  281. ]
  282. flexmock(module).should_receive('filter_hooks').with_args(
  283. commands,
  284. before='action',
  285. hook_name='myhook',
  286. action_names=['create'],
  287. ).and_return(flexmock()).once()
  288. flexmock(module).should_receive('filter_hooks').with_args(
  289. commands,
  290. after='action',
  291. hook_name='myhook',
  292. action_names=['create'],
  293. ).and_return(flexmock()).once()
  294. flexmock(module).should_receive('execute_hooks').and_raise(OSError).and_return(None)
  295. flexmock(module).should_receive('considered_soft_failure').and_return(False)
  296. with pytest.raises(ValueError):
  297. with module.Before_after_hooks(
  298. command_hooks=commands,
  299. before_after='action',
  300. umask=1234,
  301. dry_run=False,
  302. hook_name='myhook',
  303. action_names=['create'],
  304. context1='stuff',
  305. context2='such',
  306. ):
  307. assert False # This should never get called.
  308. def test_before_after_hooks_with_before_soft_failure_does_not_raise():
  309. commands = [
  310. {'before': 'repository', 'run': ['foo', 'bar']},
  311. {'after': 'repository', 'run': ['baz']},
  312. ]
  313. flexmock(module).should_receive('filter_hooks').with_args(
  314. commands,
  315. before='action',
  316. hook_name='myhook',
  317. action_names=['create'],
  318. ).and_return(flexmock()).once()
  319. flexmock(module).should_receive('filter_hooks').with_args(
  320. commands,
  321. after='action',
  322. hook_name='myhook',
  323. action_names=['create'],
  324. ).and_return(flexmock()).once()
  325. flexmock(module).should_receive('execute_hooks').and_raise(OSError)
  326. flexmock(module).should_receive('considered_soft_failure').and_return(True)
  327. with module.Before_after_hooks(
  328. command_hooks=commands,
  329. before_after='action',
  330. umask=1234,
  331. dry_run=False,
  332. hook_name='myhook',
  333. action_names=['create'],
  334. context1='stuff',
  335. context2='such',
  336. ):
  337. pass
  338. def test_before_after_hooks_with_after_error_raises():
  339. commands = [
  340. {'before': 'repository', 'run': ['foo', 'bar']},
  341. {'after': 'repository', 'run': ['baz']},
  342. ]
  343. flexmock(module).should_receive('filter_hooks').with_args(
  344. commands,
  345. before='action',
  346. hook_name='myhook',
  347. action_names=['create'],
  348. ).and_return(flexmock()).once()
  349. flexmock(module).should_receive('filter_hooks').with_args(
  350. commands,
  351. after='action',
  352. hook_name='myhook',
  353. action_names=['create'],
  354. ).and_return(flexmock()).once()
  355. flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
  356. flexmock(module).should_receive('considered_soft_failure').and_return(False)
  357. with pytest.raises(ValueError):
  358. with module.Before_after_hooks(
  359. command_hooks=commands,
  360. before_after='action',
  361. umask=1234,
  362. dry_run=False,
  363. hook_name='myhook',
  364. action_names=['create'],
  365. context1='stuff',
  366. context2='such',
  367. ):
  368. pass
  369. def test_before_after_hooks_with_after_soft_failure_does_not_raise():
  370. commands = [
  371. {'before': 'repository', 'run': ['foo', 'bar']},
  372. {'after': 'repository', 'run': ['baz']},
  373. ]
  374. flexmock(module).should_receive('filter_hooks').with_args(
  375. commands,
  376. before='action',
  377. hook_name='myhook',
  378. action_names=['create'],
  379. ).and_return(flexmock()).once()
  380. flexmock(module).should_receive('filter_hooks').with_args(
  381. commands,
  382. after='action',
  383. hook_name='myhook',
  384. action_names=['create'],
  385. ).and_return(flexmock()).once()
  386. flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
  387. flexmock(module).should_receive('considered_soft_failure').and_return(True)
  388. with module.Before_after_hooks(
  389. command_hooks=commands,
  390. before_after='action',
  391. umask=1234,
  392. dry_run=False,
  393. hook_name='myhook',
  394. action_names=['create'],
  395. context1='stuff',
  396. context2='such',
  397. ):
  398. pass
  399. def test_considered_soft_failure_treats_soft_fail_exit_code_as_soft_fail():
  400. error = subprocess.CalledProcessError(module.SOFT_FAIL_EXIT_CODE, 'try again')
  401. assert module.considered_soft_failure(error)
  402. def test_considered_soft_failure_does_not_treat_other_exit_code_as_soft_fail():
  403. error = subprocess.CalledProcessError(1, 'error')
  404. assert not module.considered_soft_failure(error)
  405. def test_considered_soft_failure_does_not_treat_other_exception_type_as_soft_fail():
  406. assert not module.considered_soft_failure(Exception())