2019-07-05 04:20:41 +08:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2016-03-30 06:30:57 +08:00
|
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
import time
|
|
|
|
import re
|
2019-07-05 04:20:41 +08:00
|
|
|
import os
|
|
|
|
import threading
|
|
|
|
|
2016-03-30 06:30:57 +08:00
|
|
|
|
|
|
|
# If a test fails, wait a moment before retrieving the captured
|
|
|
|
# stdout/stderr. When using a server process, this makes sure that we capture
|
|
|
|
# any potential output of the server that comes *after* a test has failed. For
|
|
|
|
# example, if a request handler raises an exception, the server first signals an
|
|
|
|
# error to FUSE (causing the test to fail), and then logs the exception. Without
|
|
|
|
# the extra delay, the exception will go into nowhere.
|
2023-04-07 15:34:43 +08:00
|
|
|
@pytest.hookimpl(hookwrapper=True)
|
2016-03-30 06:30:57 +08:00
|
|
|
def pytest_pyfunc_call(pyfuncitem):
|
|
|
|
outcome = yield
|
|
|
|
failed = outcome.excinfo is not None
|
|
|
|
if failed:
|
|
|
|
time.sleep(1)
|
|
|
|
|
2019-07-05 04:20:41 +08:00
|
|
|
|
|
|
|
class OutputChecker:
|
2020-06-22 13:27:58 +08:00
|
|
|
'''Check output data for suspicious patterns.
|
2019-07-05 04:20:41 +08:00
|
|
|
|
|
|
|
Everything written to check_output.fd will be scanned for suspicious
|
|
|
|
messages and then written to sys.stdout.
|
2016-03-30 06:30:57 +08:00
|
|
|
'''
|
|
|
|
|
2019-07-05 04:20:41 +08:00
|
|
|
def __init__(self):
|
|
|
|
(fd_r, fd_w) = os.pipe()
|
|
|
|
self.fd = fd_w
|
|
|
|
self._false_positives = []
|
|
|
|
self._buf = bytearray()
|
|
|
|
self._thread = threading.Thread(target=self._loop, daemon=True, args=(fd_r,))
|
|
|
|
self._thread.start()
|
|
|
|
|
|
|
|
def register_output(self, pattern, count=1, flags=re.MULTILINE):
|
|
|
|
'''Register *pattern* as false positive for output checking
|
|
|
|
|
|
|
|
This prevents the test from failing because the output otherwise
|
|
|
|
appears suspicious.
|
|
|
|
'''
|
|
|
|
|
|
|
|
self._false_positives.append((pattern, flags, count))
|
|
|
|
|
|
|
|
def _loop(self, ifd):
|
|
|
|
BUFSIZE = 128*1024
|
|
|
|
ofd = sys.stdout.fileno()
|
|
|
|
while True:
|
|
|
|
buf = os.read(ifd, BUFSIZE)
|
|
|
|
if not buf:
|
|
|
|
break
|
|
|
|
os.write(ofd, buf)
|
|
|
|
self._buf += buf
|
|
|
|
|
|
|
|
def _check(self):
|
|
|
|
os.close(self.fd)
|
|
|
|
self._thread.join()
|
|
|
|
|
|
|
|
buf = self._buf.decode('utf8', errors='replace')
|
|
|
|
|
|
|
|
# Strip out false positives
|
|
|
|
for (pattern, flags, count) in self._false_positives:
|
|
|
|
cp = re.compile(pattern, flags)
|
|
|
|
(buf, cnt) = cp.subn('', buf, count=count)
|
|
|
|
|
|
|
|
patterns = [ r'\b{}\b'.format(x) for x in
|
|
|
|
('exception', 'error', 'warning', 'fatal', 'traceback',
|
|
|
|
'fault', 'crash(?:ed)?', 'abort(?:ed)',
|
|
|
|
'uninitiali[zs]ed') ]
|
|
|
|
patterns += ['^==[0-9]+== ']
|
|
|
|
|
|
|
|
for pattern in patterns:
|
|
|
|
cp = re.compile(pattern, re.IGNORECASE | re.MULTILINE)
|
|
|
|
hit = cp.search(buf)
|
|
|
|
if hit:
|
|
|
|
raise AssertionError('Suspicious output to stderr (matched "%s")'
|
|
|
|
% hit.group(0))
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
def output_checker(request):
|
|
|
|
checker = OutputChecker()
|
|
|
|
yield checker
|
|
|
|
checker._check()
|
|
|
|
|
|
|
|
|
|
|
|
# Make test outcome available to fixtures
|
|
|
|
# (from https://github.com/pytest-dev/pytest/issues/230)
|
|
|
|
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
|
|
|
def pytest_runtest_makereport(item, call):
|
|
|
|
outcome = yield
|
|
|
|
rep = outcome.get_result()
|
|
|
|
setattr(item, "rep_" + rep.when, rep)
|
|
|
|
return rep
|