1#!/usr/bin/env python3
2
3import sys
4import pytest
5import time
6import re
7import os
8import threading
9
10
11# If a test fails, wait a moment before retrieving the captured
12# stdout/stderr. When using a server process, this makes sure that we capture
13# any potential output of the server that comes *after* a test has failed. For
14# example, if a request handler raises an exception, the server first signals an
15# error to FUSE (causing the test to fail), and then logs the exception. Without
16# the extra delay, the exception will go into nowhere.
17@pytest.mark.hookwrapper
18def pytest_pyfunc_call(pyfuncitem):
19    outcome = yield
20    failed = outcome.excinfo is not None
21    if failed:
22        time.sleep(1)
23
24
25class OutputChecker:
26    '''Check output data for suspicious patterns.
27
28    Everything written to check_output.fd will be scanned for suspicious
29    messages and then written to sys.stdout.
30    '''
31
32    def __init__(self):
33        (fd_r, fd_w) = os.pipe()
34        self.fd = fd_w
35        self._false_positives = []
36        self._buf = bytearray()
37        self._thread = threading.Thread(target=self._loop, daemon=True, args=(fd_r,))
38        self._thread.start()
39
40    def register_output(self, pattern, count=1, flags=re.MULTILINE):
41        '''Register *pattern* as false positive for output checking
42
43        This prevents the test from failing because the output otherwise
44        appears suspicious.
45        '''
46
47        self._false_positives.append((pattern, flags, count))
48
49    def _loop(self, ifd):
50        BUFSIZE = 128*1024
51        ofd = sys.stdout.fileno()
52        while True:
53            buf = os.read(ifd, BUFSIZE)
54            if not buf:
55                break
56            os.write(ofd, buf)
57            self._buf += buf
58
59    def _check(self):
60        os.close(self.fd)
61        self._thread.join()
62
63        buf = self._buf.decode('utf8', errors='replace')
64
65        # Strip out false positives
66        for (pattern, flags, count) in self._false_positives:
67            cp = re.compile(pattern, flags)
68            (buf, cnt) = cp.subn('', buf, count=count)
69
70        patterns = [ r'\b{}\b'.format(x) for x in
71                     ('exception', 'error', 'warning', 'fatal', 'traceback',
72                        'fault', 'crash(?:ed)?', 'abort(?:ed)',
73                        'uninitiali[zs]ed') ]
74        patterns += ['^==[0-9]+== ']
75
76        for pattern in patterns:
77            cp = re.compile(pattern, re.IGNORECASE | re.MULTILINE)
78            hit = cp.search(buf)
79            if hit:
80                raise AssertionError('Suspicious output to stderr (matched "%s")'
81                                     % hit.group(0))
82
83@pytest.fixture()
84def output_checker(request):
85    checker = OutputChecker()
86    yield checker
87    checker._check()
88
89
90# Make test outcome available to fixtures
91# (from https://github.com/pytest-dev/pytest/issues/230)
92@pytest.hookimpl(hookwrapper=True, tryfirst=True)
93def pytest_runtest_makereport(item, call):
94    outcome = yield
95    rep = outcome.get_result()
96    setattr(item, "rep_" + rep.when, rep)
97    return rep
98