conftest.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. """
  2. Opt-in tracing for the cloud integration suites.
  3. The AWS suite's wall time is dominated by one or two tests, and end-to-end
  4. timings have proved too noisy to attribute - test_create_and_list_image alone
  5. has measured 26.2, 60.6 and 33.8 minutes across three runs of the same 123
  6. tests. Answering *where* that time goes needs the individual waits timed
  7. rather than the test as a whole.
  8. Set CB_TEST_TRACE=1 to capture, per xdist worker:
  9. * every ``wait_for`` poll, which brackets each wait - the span from a wait's
  10. first poll to its last is the wait itself, and the gap between consecutive
  11. polls shows whether the state call is slow (the interval is 1s for real
  12. providers, so a larger gap is the API, not the sleep);
  13. * botocore retries, which is how throttling appears client-side. Those
  14. loggers stay silent unless a request is actually retried, so anything they
  15. emit is signal.
  16. Records go to cb-trace-<worker>.log rather than stderr because pytest
  17. captures stderr at file-descriptor level, and captured output is discarded
  18. for passing tests - which these are. tox prints the files once the run
  19. finishes.
  20. Deliberately opt-in: at a 1s poll interval a 30 minute wait is ~1800 lines
  21. for a single waiting resource.
  22. """
  23. import logging
  24. import os
  25. TRACE_FILE_PREFIX = 'cb-trace-'
  26. def _tracing_requested():
  27. return (os.environ.get('CB_TEST_TRACE') or '').lower() in (
  28. '1', 'true', 'yes')
  29. def _trace_path():
  30. # Each xdist worker needs its own file; they run in one directory and
  31. # would otherwise interleave mid-line.
  32. worker = os.environ.get('PYTEST_XDIST_WORKER', 'main')
  33. return '{0}{1}.log'.format(TRACE_FILE_PREFIX, worker)
  34. class _WaitAndRetryOnly(logging.Filter):
  35. """
  36. Keep the poll lines, the test markers and the retries; drop the rest.
  37. cloudbridge at DEBUG is far too chatty to keep wholesale - most of the
  38. volume is per-request logging from the provider helpers, which says
  39. nothing about where a wait went.
  40. """
  41. def filter(self, record):
  42. message = record.getMessage()
  43. if record.name.startswith('botocore'):
  44. # botocore logs a line per request either way; only the ones
  45. # where it actually backed off say anything about throttling.
  46. return not message.startswith('Not retrying')
  47. return message.startswith('=== ') or 'Waiting another' in message
  48. def pytest_configure(config):
  49. if not _tracing_requested():
  50. return
  51. handler = logging.FileHandler(_trace_path(), mode='w')
  52. handler.setFormatter(logging.Formatter(
  53. '%(asctime)s %(name)s %(message)s'))
  54. handler.addFilter(_WaitAndRetryOnly())
  55. # wait_for logs one line per poll at DEBUG, naming the object and the
  56. # state it is waiting on. Scope the level to that module rather than the
  57. # cloudbridge tree: raising the whole tree to DEBUG would have every
  58. # provider request build a log record that the filter then discards, and
  59. # those records still propagate to pytest's own capture handler, which
  60. # holds them for the duration of the test.
  61. waits = logging.getLogger('cloudbridge.base.resources')
  62. waits.addHandler(handler)
  63. waits.setLevel(logging.DEBUG)
  64. # Quiet unless a request is actually retried, which is how throttling
  65. # shows up client-side. Enabling botocore wholesale would log every
  66. # request and response.
  67. for name in ('botocore.retries', 'botocore.retryhandler'):
  68. target = logging.getLogger(name)
  69. target.addHandler(handler)
  70. target.setLevel(logging.DEBUG)
  71. def pytest_runtest_logstart(nodeid, location):
  72. # Stamps the trace with test boundaries, so a span of polls can be
  73. # attributed to the test that caused it.
  74. if _tracing_requested():
  75. logging.getLogger('cloudbridge.base.resources').debug(
  76. '=== START %s', nodeid)
  77. def pytest_runtest_logfinish(nodeid, location):
  78. if _tracing_requested():
  79. logging.getLogger('cloudbridge.base.resources').debug(
  80. '=== END %s', nodeid)