/src/cosmic_ray/testing.py

https://github.com/sixty-north/cosmic-ray · Python · 79 lines · 52 code · 14 blank · 13 comment · 8 complexity · 44b7f9e561ccf71bdfd1ec9bc551e9ef MD5 · raw file

  1. "Support for running tests in a subprocess."
  2. import asyncio
  3. import os
  4. import sys
  5. import traceback
  6. from cosmic_ray.work_item import TestOutcome
  7. # We use an asyncio-subprocess-based approach here instead of a simple
  8. # subprocess.run()-based approach because there are problems with timeouts and
  9. # reading from stderr in subprocess.run. Since we have to be prepared for test
  10. # processes that run longer than timeout (and, indeed, which run forever), the
  11. # broken subprocess stuff simply doesn't work. So we do this, which seesm to
  12. # work on all platforms.
  13. async def _run_tests(command, timeout):
  14. # We want to avoid writing pyc files in case our changes happen too fast for Python to
  15. # notice them. If the timestamps between two changes are too small, Python won't recompile
  16. # the source.
  17. env = dict(os.environ)
  18. env['PYTHONDONTWRITEBYTECODE'] = '1'
  19. try:
  20. proc = await asyncio.create_subprocess_shell(
  21. command,
  22. stdout=asyncio.subprocess.PIPE,
  23. stderr=asyncio.subprocess.STDOUT,
  24. env=env)
  25. except Exception: # pylint: disable=W0703
  26. return (TestOutcome.INCOMPETENT, traceback.format_exc())
  27. try:
  28. outs, _errs = await asyncio.wait_for(proc.communicate(), timeout)
  29. assert proc.returncode is not None
  30. if proc.returncode == 0:
  31. return (TestOutcome.SURVIVED, outs.decode('utf-8'))
  32. else:
  33. return (TestOutcome.KILLED, outs.decode('utf-8'))
  34. except asyncio.TimeoutError:
  35. proc.terminate()
  36. return (TestOutcome.KILLED, 'timeout')
  37. except Exception: # pylint: disable=W0703
  38. proc.terminate()
  39. return (TestOutcome.INCOMPETENT, traceback.format_exc())
  40. finally:
  41. await proc.wait()
  42. def run_tests(command, timeout=None):
  43. """Run test command in a subprocess.
  44. If the command exits with status 0, then we assume that all tests passed. If
  45. it exits with any other code, we assume a test failed. If the call to launch
  46. the subprocess throws an exception, we consider the test 'incompetent'.
  47. Tests which time out are considered 'killed' as well.
  48. Args:
  49. command (str): The command to execute.
  50. timeout (number): The maximum number of seconds to allow the tests to run.
  51. Return: A tuple `(TestOutcome, output)` where the `output` is a string
  52. containing the output of the command.
  53. """
  54. if sys.platform == "win32":
  55. asyncio.set_event_loop_policy(
  56. asyncio.WindowsProactorEventLoopPolicy())
  57. result = asyncio.get_event_loop().run_until_complete(
  58. _run_tests(command, timeout))
  59. return result