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