test_execute.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. import logging
  2. import subprocess
  3. import sys
  4. import pytest
  5. from flexmock import flexmock
  6. from borgmatic import execute as module
  7. def test_log_outputs_logs_each_line_separately():
  8. flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'hi').once()
  9. flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'there').once()
  10. flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS)
  11. hi_process = subprocess.Popen(['echo', 'hi'], stdout=subprocess.PIPE)
  12. flexmock(module).should_receive('output_buffer_for_process').with_args(
  13. hi_process, ()
  14. ).and_return(hi_process.stdout)
  15. there_process = subprocess.Popen(['echo', 'there'], stdout=subprocess.PIPE)
  16. flexmock(module).should_receive('output_buffer_for_process').with_args(
  17. there_process, ()
  18. ).and_return(there_process.stdout)
  19. module.log_outputs(
  20. (hi_process, there_process),
  21. exclude_stdouts=(),
  22. output_log_level=logging.INFO,
  23. borg_local_path='borg',
  24. borg_exit_codes=None,
  25. )
  26. def test_log_outputs_skips_logs_for_process_with_none_stdout():
  27. flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'hi').never()
  28. flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'there').once()
  29. flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS)
  30. hi_process = subprocess.Popen(['echo', 'hi'], stdout=None)
  31. flexmock(module).should_receive('output_buffer_for_process').with_args(
  32. hi_process, ()
  33. ).and_return(hi_process.stdout)
  34. there_process = subprocess.Popen(['echo', 'there'], stdout=subprocess.PIPE)
  35. flexmock(module).should_receive('output_buffer_for_process').with_args(
  36. there_process, ()
  37. ).and_return(there_process.stdout)
  38. module.log_outputs(
  39. (hi_process, there_process),
  40. exclude_stdouts=(),
  41. output_log_level=logging.INFO,
  42. borg_local_path='borg',
  43. borg_exit_codes=None,
  44. )
  45. def test_log_outputs_returns_output_without_logging_for_output_log_level_none():
  46. flexmock(module.logger).should_receive('log').never()
  47. flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS)
  48. hi_process = subprocess.Popen(['echo', 'hi'], stdout=subprocess.PIPE)
  49. flexmock(module).should_receive('output_buffer_for_process').with_args(
  50. hi_process, ()
  51. ).and_return(hi_process.stdout)
  52. there_process = subprocess.Popen(['echo', 'there'], stdout=subprocess.PIPE)
  53. flexmock(module).should_receive('output_buffer_for_process').with_args(
  54. there_process, ()
  55. ).and_return(there_process.stdout)
  56. captured_outputs = module.log_outputs(
  57. (hi_process, there_process),
  58. exclude_stdouts=(),
  59. output_log_level=None,
  60. borg_local_path='borg',
  61. borg_exit_codes=None,
  62. )
  63. assert captured_outputs == {hi_process: 'hi', there_process: 'there'}
  64. def test_log_outputs_includes_error_output_in_exception():
  65. flexmock(module.logger).should_receive('log')
  66. flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
  67. flexmock(module).should_receive('command_for_process').and_return('grep')
  68. process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  69. flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
  70. with pytest.raises(subprocess.CalledProcessError) as error:
  71. module.log_outputs(
  72. (process,),
  73. exclude_stdouts=(),
  74. output_log_level=logging.INFO,
  75. borg_local_path='borg',
  76. borg_exit_codes=None,
  77. )
  78. assert error.value.output
  79. def test_log_outputs_logs_multiline_error_output():
  80. '''
  81. Make sure that all error output lines get logged, not just (for instance) the first few lines
  82. of a process' traceback.
  83. '''
  84. flexmock(module.logger).should_receive('log')
  85. flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
  86. flexmock(module).should_receive('command_for_process').and_return('grep')
  87. process = subprocess.Popen(
  88. ['python', '-c', 'foopydoo'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
  89. )
  90. flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
  91. flexmock(module.logger).should_call('log').at_least().times(3)
  92. with pytest.raises(subprocess.CalledProcessError):
  93. module.log_outputs(
  94. (process,),
  95. exclude_stdouts=(),
  96. output_log_level=logging.INFO,
  97. borg_local_path='borg',
  98. borg_exit_codes=None,
  99. )
  100. def test_log_outputs_skips_error_output_in_exception_for_process_with_none_stdout():
  101. flexmock(module.logger).should_receive('log')
  102. flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
  103. flexmock(module).should_receive('command_for_process').and_return('grep')
  104. process = subprocess.Popen(['grep'], stdout=None)
  105. flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
  106. with pytest.raises(subprocess.CalledProcessError) as error:
  107. module.log_outputs(
  108. (process,),
  109. exclude_stdouts=(),
  110. output_log_level=logging.INFO,
  111. borg_local_path='borg',
  112. borg_exit_codes=None,
  113. )
  114. assert error.value.returncode == 2
  115. assert not error.value.output
  116. def test_log_outputs_kills_other_processes_and_raises_when_one_errors():
  117. flexmock(module.logger).should_receive('log')
  118. flexmock(module).should_receive('command_for_process').and_return('grep')
  119. process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  120. flexmock(module).should_receive('interpret_exit_code').with_args(
  121. ['grep'],
  122. None,
  123. 'borg',
  124. None,
  125. ).and_return(module.Exit_status.SUCCESS)
  126. flexmock(module).should_receive('interpret_exit_code').with_args(
  127. ['grep'],
  128. 2,
  129. 'borg',
  130. None,
  131. ).and_return(module.Exit_status.ERROR)
  132. other_process = subprocess.Popen(
  133. ['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
  134. )
  135. flexmock(module).should_receive('interpret_exit_code').with_args(
  136. ['sleep', '2'],
  137. None,
  138. 'borg',
  139. None,
  140. ).and_return(module.Exit_status.SUCCESS)
  141. flexmock(module).should_receive('output_buffer_for_process').with_args(process, ()).and_return(
  142. process.stdout
  143. )
  144. flexmock(module).should_receive('output_buffer_for_process').with_args(
  145. other_process, ()
  146. ).and_return(other_process.stdout)
  147. flexmock(other_process).should_receive('kill').once()
  148. with pytest.raises(subprocess.CalledProcessError) as error:
  149. module.log_outputs(
  150. (process, other_process),
  151. exclude_stdouts=(),
  152. output_log_level=logging.INFO,
  153. borg_local_path='borg',
  154. borg_exit_codes=None,
  155. )
  156. assert error.value.returncode == 2
  157. assert error.value.output
  158. def test_log_outputs_kills_other_processes_and_returns_when_one_exits_with_warning():
  159. flexmock(module.logger).should_receive('log')
  160. flexmock(module).should_receive('command_for_process').and_return('grep')
  161. process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  162. flexmock(module).should_receive('interpret_exit_code').with_args(
  163. ['grep'],
  164. None,
  165. 'borg',
  166. None,
  167. ).and_return(module.Exit_status.SUCCESS)
  168. flexmock(module).should_receive('interpret_exit_code').with_args(
  169. ['grep'],
  170. 2,
  171. 'borg',
  172. None,
  173. ).and_return(module.Exit_status.WARNING)
  174. other_process = subprocess.Popen(
  175. ['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
  176. )
  177. flexmock(module).should_receive('interpret_exit_code').with_args(
  178. ['sleep', '2'],
  179. None,
  180. 'borg',
  181. None,
  182. ).and_return(module.Exit_status.SUCCESS)
  183. flexmock(module).should_receive('output_buffer_for_process').with_args(process, ()).and_return(
  184. process.stdout
  185. )
  186. flexmock(module).should_receive('output_buffer_for_process').with_args(
  187. other_process, ()
  188. ).and_return(other_process.stdout)
  189. flexmock(other_process).should_receive('kill').once()
  190. module.log_outputs(
  191. (process, other_process),
  192. exclude_stdouts=(),
  193. output_log_level=logging.INFO,
  194. borg_local_path='borg',
  195. borg_exit_codes=None,
  196. )
  197. def test_log_outputs_vents_other_processes_when_one_exits():
  198. '''
  199. Execute a command to generate a longish random string and pipe it into another command that
  200. exits quickly. The test is basically to ensure we don't hang forever waiting for the exited
  201. process to read the pipe, and that the string-generating process eventually gets vented and
  202. exits.
  203. '''
  204. flexmock(module.logger).should_receive('log')
  205. flexmock(module).should_receive('command_for_process').and_return('grep')
  206. process = subprocess.Popen(
  207. [
  208. sys.executable,
  209. '-c',
  210. "import random, string; print(''.join(random.choice(string.ascii_letters) for _ in range(40000)))",
  211. ],
  212. stdout=subprocess.PIPE,
  213. stderr=subprocess.PIPE,
  214. )
  215. other_process = subprocess.Popen(
  216. ['true'], stdin=process.stdout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
  217. )
  218. flexmock(module).should_receive('output_buffer_for_process').with_args(
  219. process, (process.stdout,)
  220. ).and_return(process.stderr)
  221. flexmock(module).should_receive('output_buffer_for_process').with_args(
  222. other_process, (process.stdout,)
  223. ).and_return(other_process.stdout)
  224. flexmock(process.stdout).should_call('readline').at_least().once()
  225. module.log_outputs(
  226. (process, other_process),
  227. exclude_stdouts=(process.stdout,),
  228. output_log_level=logging.INFO,
  229. borg_local_path='borg',
  230. borg_exit_codes=None,
  231. )
  232. def test_log_outputs_does_not_error_when_one_process_exits():
  233. flexmock(module.logger).should_receive('log')
  234. flexmock(module).should_receive('command_for_process').and_return('grep')
  235. process = subprocess.Popen(
  236. [
  237. sys.executable,
  238. '-c',
  239. "import random, string; print(''.join(random.choice(string.ascii_letters) for _ in range(40000)))",
  240. ],
  241. stdout=None, # Specifically test the case of a process without stdout captured.
  242. stderr=None,
  243. )
  244. other_process = subprocess.Popen(
  245. ['true'], stdin=process.stdout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
  246. )
  247. flexmock(module).should_receive('output_buffer_for_process').with_args(
  248. process, (process.stdout,)
  249. ).and_return(process.stderr)
  250. flexmock(module).should_receive('output_buffer_for_process').with_args(
  251. other_process, (process.stdout,)
  252. ).and_return(other_process.stdout)
  253. module.log_outputs(
  254. (process, other_process),
  255. exclude_stdouts=(process.stdout,),
  256. output_log_level=logging.INFO,
  257. borg_local_path='borg',
  258. borg_exit_codes=None,
  259. )
  260. def test_log_outputs_truncates_long_error_output():
  261. flexmock(module.logger).should_receive('log')
  262. flexmock(module).should_receive('command_for_process').and_return('grep')
  263. process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  264. flexmock(module).should_receive('interpret_exit_code').with_args(
  265. ['grep'],
  266. None,
  267. 'borg',
  268. None,
  269. ).and_return(module.Exit_status.SUCCESS)
  270. flexmock(module).should_receive('interpret_exit_code').with_args(
  271. ['grep'],
  272. 2,
  273. 'borg',
  274. None,
  275. ).and_return(module.Exit_status.ERROR)
  276. flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
  277. with pytest.raises(subprocess.CalledProcessError) as error:
  278. flexmock(module, ERROR_OUTPUT_MAX_LINE_COUNT=0).log_outputs(
  279. (process,),
  280. exclude_stdouts=(),
  281. output_log_level=logging.INFO,
  282. borg_local_path='borg',
  283. borg_exit_codes=None,
  284. )
  285. assert error.value.returncode == 2
  286. assert error.value.output.startswith('...')
  287. def test_log_outputs_with_no_output_logs_nothing():
  288. flexmock(module.logger).should_receive('log').never()
  289. flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS)
  290. process = subprocess.Popen(['true'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  291. flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
  292. module.log_outputs(
  293. (process,),
  294. exclude_stdouts=(),
  295. output_log_level=logging.INFO,
  296. borg_local_path='borg',
  297. borg_exit_codes=None,
  298. )
  299. def test_log_outputs_with_unfinished_process_re_polls():
  300. flexmock(module.logger).should_receive('log').never()
  301. flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS)
  302. process = subprocess.Popen(['true'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  303. flexmock(process).should_receive('poll').and_return(None).and_return(0).times(3)
  304. flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
  305. module.log_outputs(
  306. (process,),
  307. exclude_stdouts=(),
  308. output_log_level=logging.INFO,
  309. borg_local_path='borg',
  310. borg_exit_codes=None,
  311. )