2
0

test_execute.py 14 KB

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