2
0

test_command.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  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. working_directory=None,
  168. ).once()
  169. module.execute_hooks(
  170. [{'before': 'create', 'run': ['foo']}, {'before': 'create', 'run': ['bar', 'baz']}],
  171. umask=None,
  172. working_directory=None,
  173. dry_run=False,
  174. )
  175. def test_execute_hooks_with_umask_sets_that_umask():
  176. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  177. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  178. flexmock(module).should_receive('interpolate_context').replace_with(
  179. lambda hook_description, command, context: command
  180. )
  181. flexmock(module.os).should_receive('umask').with_args(0o77).and_return(0o22).once()
  182. flexmock(module.os).should_receive('umask').with_args(0o22).once()
  183. flexmock(module).should_receive('make_environment').and_return({})
  184. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  185. ['foo'],
  186. output_log_level=logging.ANSWER,
  187. shell=True,
  188. environment={},
  189. working_directory=None,
  190. )
  191. module.execute_hooks(
  192. [{'before': 'create', 'run': ['foo']}], umask=77, working_directory=None, dry_run=False
  193. )
  194. def test_execute_hooks_with_working_directory_executes_command_with_it():
  195. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  196. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  197. flexmock(module).should_receive('interpolate_context').replace_with(
  198. lambda hook_description, command, context: command
  199. )
  200. flexmock(module).should_receive('make_environment').and_return({})
  201. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  202. ['foo'],
  203. output_log_level=logging.ANSWER,
  204. shell=True,
  205. environment={},
  206. working_directory='/working',
  207. )
  208. module.execute_hooks(
  209. [{'before': 'create', 'run': ['foo']}],
  210. umask=None,
  211. working_directory='/working',
  212. dry_run=False,
  213. )
  214. def test_execute_hooks_with_dry_run_skips_commands():
  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').replace_with(
  218. lambda hook_description, command, context: command
  219. )
  220. flexmock(module).should_receive('make_environment').and_return({})
  221. flexmock(module.borgmatic.execute).should_receive('execute_command').never()
  222. module.execute_hooks(
  223. [{'before': 'create', 'run': ['foo']}], umask=None, working_directory=None, dry_run=True
  224. )
  225. def test_execute_hooks_with_empty_commands_does_not_raise():
  226. module.execute_hooks([], umask=None, working_directory=None, dry_run=True)
  227. def test_execute_hooks_with_error_logs_as_error():
  228. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  229. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  230. flexmock(module).should_receive('interpolate_context').replace_with(
  231. lambda hook_description, command, context: command
  232. )
  233. flexmock(module).should_receive('make_environment').and_return({})
  234. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  235. ['foo'],
  236. output_log_level=logging.ERROR,
  237. shell=True,
  238. environment={},
  239. working_directory=None,
  240. ).once()
  241. module.execute_hooks(
  242. [{'after': 'error', 'run': ['foo']}], umask=None, working_directory=None, dry_run=False
  243. )
  244. def test_execute_hooks_with_before_or_after_raises():
  245. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  246. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  247. flexmock(module).should_receive('interpolate_context').never()
  248. flexmock(module).should_receive('make_environment').never()
  249. flexmock(module.borgmatic.execute).should_receive('execute_command').never()
  250. with pytest.raises(ValueError):
  251. module.execute_hooks(
  252. [
  253. {'erstwhile': 'create', 'run': ['foo']},
  254. {'erstwhile': 'create', 'run': ['bar', 'baz']},
  255. ],
  256. umask=None,
  257. working_directory=None,
  258. dry_run=False,
  259. )
  260. def test_execute_hooks_without_commands_to_run_does_not_raise():
  261. flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
  262. flexmock(module.logging).ANSWER = LOGGING_ANSWER
  263. flexmock(module).should_receive('interpolate_context').replace_with(
  264. lambda hook_description, command, context: command
  265. )
  266. flexmock(module).should_receive('make_environment').and_return({})
  267. for command in ('foo', 'bar'):
  268. flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
  269. [command],
  270. output_log_level=LOGGING_ANSWER,
  271. shell=True,
  272. environment={},
  273. working_directory=None,
  274. ).once()
  275. module.execute_hooks(
  276. [{'before': 'create', 'run': []}, {'before': 'create', 'run': ['foo', 'bar']}],
  277. umask=None,
  278. working_directory=None,
  279. dry_run=False,
  280. )
  281. def test_before_after_hooks_calls_command_hooks():
  282. commands = [
  283. {'before': 'repository', 'run': ['foo', 'bar']},
  284. {'after': 'repository', 'run': ['baz']},
  285. ]
  286. flexmock(module).should_receive('filter_hooks').with_args(
  287. commands,
  288. before='action',
  289. hook_name='myhook',
  290. action_names=['create'],
  291. ).and_return(flexmock()).once()
  292. flexmock(module).should_receive('filter_hooks').with_args(
  293. commands,
  294. after='action',
  295. hook_name='myhook',
  296. action_names=['create'],
  297. ).and_return(flexmock()).once()
  298. flexmock(module).should_receive('execute_hooks').twice()
  299. with module.Before_after_hooks(
  300. command_hooks=commands,
  301. before_after='action',
  302. umask=1234,
  303. working_directory='/working',
  304. dry_run=False,
  305. hook_name='myhook',
  306. action_names=['create'],
  307. context1='stuff',
  308. context2='such',
  309. ):
  310. pass
  311. def test_before_after_hooks_with_before_error_runs_after_hook_and_raises():
  312. commands = [
  313. {'before': 'repository', 'run': ['foo', 'bar']},
  314. {'after': 'repository', 'run': ['baz']},
  315. ]
  316. flexmock(module).should_receive('filter_hooks').with_args(
  317. commands,
  318. before='action',
  319. hook_name='myhook',
  320. action_names=['create'],
  321. ).and_return(flexmock()).once()
  322. flexmock(module).should_receive('filter_hooks').with_args(
  323. commands,
  324. after='action',
  325. hook_name='myhook',
  326. action_names=['create'],
  327. ).and_return(flexmock()).once()
  328. flexmock(module).should_receive('execute_hooks').and_raise(OSError).and_return(None)
  329. flexmock(module).should_receive('considered_soft_failure').and_return(False)
  330. with pytest.raises(ValueError):
  331. with module.Before_after_hooks(
  332. command_hooks=commands,
  333. before_after='action',
  334. umask=1234,
  335. working_directory='/working',
  336. dry_run=False,
  337. hook_name='myhook',
  338. action_names=['create'],
  339. context1='stuff',
  340. context2='such',
  341. ):
  342. assert False # This should never get called.
  343. def test_before_after_hooks_with_before_soft_failure_does_not_raise():
  344. commands = [
  345. {'before': 'repository', 'run': ['foo', 'bar']},
  346. {'after': 'repository', 'run': ['baz']},
  347. ]
  348. flexmock(module).should_receive('filter_hooks').with_args(
  349. commands,
  350. before='action',
  351. hook_name='myhook',
  352. action_names=['create'],
  353. ).and_return(flexmock()).once()
  354. flexmock(module).should_receive('filter_hooks').with_args(
  355. commands,
  356. after='action',
  357. hook_name='myhook',
  358. action_names=['create'],
  359. ).and_return(flexmock()).once()
  360. flexmock(module).should_receive('execute_hooks').and_raise(OSError)
  361. flexmock(module).should_receive('considered_soft_failure').and_return(True)
  362. with module.Before_after_hooks(
  363. command_hooks=commands,
  364. before_after='action',
  365. umask=1234,
  366. working_directory='/working',
  367. dry_run=False,
  368. hook_name='myhook',
  369. action_names=['create'],
  370. context1='stuff',
  371. context2='such',
  372. ):
  373. pass
  374. def test_before_after_hooks_with_after_error_raises():
  375. commands = [
  376. {'before': 'repository', 'run': ['foo', 'bar']},
  377. {'after': 'repository', 'run': ['baz']},
  378. ]
  379. flexmock(module).should_receive('filter_hooks').with_args(
  380. commands,
  381. before='action',
  382. hook_name='myhook',
  383. action_names=['create'],
  384. ).and_return(flexmock()).once()
  385. flexmock(module).should_receive('filter_hooks').with_args(
  386. commands,
  387. after='action',
  388. hook_name='myhook',
  389. action_names=['create'],
  390. ).and_return(flexmock()).once()
  391. flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
  392. flexmock(module).should_receive('considered_soft_failure').and_return(False)
  393. with pytest.raises(ValueError):
  394. with module.Before_after_hooks(
  395. command_hooks=commands,
  396. before_after='action',
  397. umask=1234,
  398. working_directory='/working',
  399. dry_run=False,
  400. hook_name='myhook',
  401. action_names=['create'],
  402. context1='stuff',
  403. context2='such',
  404. ):
  405. pass
  406. def test_before_after_hooks_with_after_soft_failure_does_not_raise():
  407. commands = [
  408. {'before': 'repository', 'run': ['foo', 'bar']},
  409. {'after': 'repository', 'run': ['baz']},
  410. ]
  411. flexmock(module).should_receive('filter_hooks').with_args(
  412. commands,
  413. before='action',
  414. hook_name='myhook',
  415. action_names=['create'],
  416. ).and_return(flexmock()).once()
  417. flexmock(module).should_receive('filter_hooks').with_args(
  418. commands,
  419. after='action',
  420. hook_name='myhook',
  421. action_names=['create'],
  422. ).and_return(flexmock()).once()
  423. flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
  424. flexmock(module).should_receive('considered_soft_failure').and_return(True)
  425. with module.Before_after_hooks(
  426. command_hooks=commands,
  427. before_after='action',
  428. umask=1234,
  429. working_directory='/working',
  430. dry_run=False,
  431. hook_name='myhook',
  432. action_names=['create'],
  433. context1='stuff',
  434. context2='such',
  435. ):
  436. pass
  437. def test_considered_soft_failure_treats_soft_fail_exit_code_as_soft_fail():
  438. error = subprocess.CalledProcessError(module.SOFT_FAIL_EXIT_CODE, 'try again')
  439. assert module.considered_soft_failure(error)
  440. def test_considered_soft_failure_does_not_treat_other_exit_code_as_soft_fail():
  441. error = subprocess.CalledProcessError(1, 'error')
  442. assert not module.considered_soft_failure(error)
  443. def test_considered_soft_failure_does_not_treat_other_exception_type_as_soft_fail():
  444. assert not module.considered_soft_failure(Exception())