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