1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
|
"""
This module scans all ``*.rst`` files below ``docs/`` for example code.
Example code is discoved by checking for lines containing the ``..
literalinclude:: `` directives.
An example consists of two consecutive literalinclude directives. The
first must include a ``*.py`` file and the second a ``*.out`` file. The
``*.py`` file consists of the example code which is executed in
a separate process. The output of this process is compared to the
contents of the ``*.out`` file.
"""
import subprocess
import sys
from _pytest.assertion.util import _diff_text
from py._code.code import TerminalRepr
import pytest
def pytest_collect_file(path, parent):
"""Checks if the file is a rst file and creates an
:class:`ExampleFile` instance."""
if path.ext == '.py' and path.dirname.endswith('code'):
return ExampleFile(path, parent)
class ExampleFile(pytest.File):
"""Represents an example ``.py`` and its output ``.out``."""
def collect(self):
pyfile = self.fspath
# Python 2's random number generator produces different numbers
# than Python 3's. Use a separate out-file for examples using
# random numbers.
if sys.version_info[0] < 3 and pyfile.new(ext='.out2').check():
outfile = pyfile.new(ext='.out2')
else:
outfile = pyfile.new(ext='.out')
if outfile.check():
yield ExampleItem(pyfile, outfile, self)
class ExampleItem(pytest.Item):
"""Executes an example found in a rst-file."""
def __init__(self, pyfile, outfile, parent):
pytest.Item.__init__(self, str(pyfile), parent)
self.pyfile = pyfile
self.outfile = outfile
def runtest(self):
# Read expected output.
with self.outfile.open() as f:
expected = f.read()
output = subprocess.check_output([sys.executable, str(self.pyfile)],
stderr=subprocess.STDOUT,
universal_newlines=True)
if output != expected:
# Hijack the ValueError exception to identify mismatching output.
raise ValueError(expected, output)
def repr_failure(self, exc_info):
if exc_info.errisinstance(ValueError):
# Output is mismatching. Create a nice diff as failure description.
expected, output = exc_info.value.args
message = _diff_text(output, expected)
return ReprFailExample(self.pyfile, self.outfile, message)
elif exc_info.errisinstance(subprocess.CalledProcessError):
# Something wrent wrong while executing the example.
return ReprErrorExample(self.pyfile, exc_info)
else:
# Something went terribly wrong :(
return pytest.Item.repr_failure(self, exc_info)
def reportinfo(self):
return self.fspath, None, '%s example' % self.pyfile.purebasename
class ReprFailExample(TerminalRepr):
"""Reports output mismatches in a nice and informative representation."""
Markup = {
'+': dict(green=True),
'-': dict(red=True),
'?': dict(bold=True),
}
"""Colorization codes for the diff markup."""
def __init__(self, pyfile, outfile, message):
self.pyfile = pyfile
self.outfile = outfile
self.message = message
def toterminal(self, tw):
for line in self.message:
markup = ReprFailExample.Markup.get(line[0], {})
tw.line(line, **markup)
tw.line('')
tw.line('%s: Unexpected output' % (self.pyfile))
class ReprErrorExample(TerminalRepr):
"""Reports failures in the execution of an example."""
def __init__(self, pyfile, exc_info):
self.pyfile = pyfile
self.exc_info = exc_info
def toterminal(self, tw):
tw.line('Execution of %s failed. Captured output:' %
self.pyfile.basename, red=True, bold=True)
tw.sep('-')
tw.line(self.exc_info.value.output)
tw.line('%s: Example failed (exitcode=%d)' %
(self.pyfile, self.exc_info.value.returncode))
|