Update to pytest 3.2.5 (#6801)

* Update to pytest 3.2.5

* Get pytest to pass Python 2.6 compatibility checks
This commit is contained in:
Adam J. Stewart 2018-01-10 17:41:50 -06:00 committed by GitHub
parent 10ee7d6d81
commit 57c71aea89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 3306 additions and 2112 deletions

View File

@ -1,141 +0,0 @@
Holger Krekel, holger at merlinux eu
merlinux GmbH, Germany, office at merlinux eu
Contributors include::
Abdeali JK
Abhijeet Kasurde
Ahn Ki-Wook
Alexei Kozlenok
Anatoly Bubenkoff
Andreas Zeidler
Andrzej Ostrowski
Andy Freeland
Anthon van der Neut
Antony Lee
Armin Rigo
Aron Curzon
Aviv Palivoda
Ben Webb
Benjamin Peterson
Bernard Pratz
Bob Ippolito
Brian Dorsey
Brian Okken
Brianna Laugher
Bruno Oliveira
Cal Leeming
Carl Friedrich Bolz
Charles Cloud
Charnjit SiNGH (CCSJ)
Chris Lamb
Christian Boelsen
Christian Theunert
Christian Tismer
Christopher Gilling
Daniel Grana
Daniel Hahler
Daniel Nuri
Daniel Wandschneider
Danielle Jenkins
Dave Hunt
David Díaz-Barquero
David Mohr
David Vierra
Diego Russo
Dmitry Dygalo
Duncan Betts
Edison Gustavo Muenz
Edoardo Batini
Eduardo Schettino
Elizaveta Shashkova
Endre Galaczi
Eric Hunsberger
Eric Siegerman
Erik M. Bray
Feng Ma
Florian Bruhin
Floris Bruynooghe
Gabriel Reis
Georgy Dyuldin
Graham Horler
Greg Price
Grig Gheorghiu
Grigorii Eremeev (budulianin)
Guido Wesdorp
Harald Armin Massa
Ian Bicking
Jaap Broekhuizen
Jan Balster
Janne Vanhala
Jason R. Coombs
Javier Domingo Cansino
Javier Romero
John Towler
Jon Sonesen
Jordan Guymon
Joshua Bronson
Jurko Gospodnetić
Justyna Janczyszyn
Kale Kundert
Katarzyna Jachim
Kevin Cox
Lee Kamentsky
Lev Maximov
Lukas Bednar
Luke Murphy
Maciek Fijalkowski
Maho
Marc Schlaich
Marcin Bachry
Mark Abramowitz
Markus Unterwaditzer
Martijn Faassen
Martin K. Scherer
Martin Prusse
Mathieu Clabaut
Matt Bachmann
Matt Williams
Matthias Hafner
mbyt
Michael Aquilina
Michael Birtwell
Michael Droettboom
Michael Seifert
Mike Lundy
Ned Batchelder
Neven Mundar
Nicolas Delaby
Oleg Pidsadnyi
Oliver Bestwalter
Omar Kohl
Pieter Mulder
Piotr Banaszkiewicz
Punyashloka Biswal
Quentin Pradet
Ralf Schmitt
Raphael Pierzina
Raquel Alegre
Roberto Polli
Romain Dorgueil
Roman Bolshakov
Ronny Pfannschmidt
Ross Lawley
Russel Winder
Ryan Wooden
Samuele Pedroni
Simon Gomizelj
Stefan Farmbauer
Stefan Zimmermann
Stefano Taschini
Steffen Allner
Stephan Obermann
Tareq Alayan
Ted Xiao
Thomas Grainger
Tom Viner
Trevor Bekolay
Tyler Goodlet
Vasily Kuznetsov
Wouter van Ackooy
Xuecong Liao

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2004-2016 Holger Krekel and others
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,102 +0,0 @@
.. image:: http://docs.pytest.org/en/latest/_static/pytest1.png
:target: http://docs.pytest.org
:align: center
:alt: pytest
------
.. image:: https://img.shields.io/pypi/v/pytest.svg
:target: https://pypi.python.org/pypi/pytest
.. image:: https://img.shields.io/pypi/pyversions/pytest.svg
:target: https://pypi.python.org/pypi/pytest
.. image:: https://img.shields.io/coveralls/pytest-dev/pytest/master.svg
:target: https://coveralls.io/r/pytest-dev/pytest
.. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master
:target: https://travis-ci.org/pytest-dev/pytest
.. image:: https://ci.appveyor.com/api/projects/status/mrgbjaua7t33pg6b?svg=true
:target: https://ci.appveyor.com/project/pytestbot/pytest
The ``pytest`` framework makes it easy to write small tests, yet
scales to support complex functional testing for applications and libraries.
An example of a simple test:
.. code-block:: python
# content of test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
To execute it::
$ pytest
============================= test session starts =============================
collected 1 items
test_sample.py F
================================== FAILURES ===================================
_________________________________ test_answer _________________________________
def test_answer():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_sample.py:5: AssertionError
========================== 1 failed in 0.04 seconds ===========================
Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See `getting-started <http://docs.pytest.org/en/latest/getting-started.html#our-first-test-run>`_ for more examples.
Features
--------
- Detailed info on failing `assert statements <http://docs.pytest.org/en/latest/assert.html>`_ (no need to remember ``self.assert*`` names);
- `Auto-discovery
<http://docs.pytest.org/en/latest/goodpractices.html#python-test-discovery>`_
of test modules and functions;
- `Modular fixtures <http://docs.pytest.org/en/latest/fixture.html>`_ for
managing small or parametrized long-lived test resources;
- Can run `unittest <http://docs.pytest.org/en/latest/unittest.html>`_ (or trial),
`nose <http://docs.pytest.org/en/latest/nose.html>`_ test suites out of the box;
- Python2.6+, Python3.3+, PyPy-2.3, Jython-2.5 (untested);
- Rich plugin architecture, with over 150+ `external plugins <http://docs.pytest.org/en/latest/plugins.html#installing-external-plugins-searching>`_ and thriving community;
Documentation
-------------
For full documentation, including installation, tutorials and PDF documents, please see http://docs.pytest.org.
Bugs/Requests
-------------
Please use the `GitHub issue tracker <https://github.com/pytest-dev/pytest/issues>`_ to submit bugs or request features.
Changelog
---------
Consult the `Changelog <http://docs.pytest.org/en/latest/changelog.html>`__ page for fixes and enhancements of each version.
License
-------
Copyright Holger Krekel and others, 2004-2016.
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE

View File

@ -1,2 +1,8 @@
# __all__ = ['__version__']
__version__ = '3.0.5'
try:
from ._version import version as __version__
except ImportError:
# broken installation, we don't even try
# unknown only works because we do poor mans version compare
__version__ = 'unknown'

View File

@ -57,26 +57,29 @@
which should throw a KeyError: 'COMPLINE' (which is properly set by the which should throw a KeyError: 'COMPLINE' (which is properly set by the
global argcomplete script). global argcomplete script).
""" """
from __future__ import absolute_import, division, print_function
import sys import sys
import os import os
from glob import glob from glob import glob
class FastFilesCompleter: class FastFilesCompleter:
'Fast file completer class' 'Fast file completer class'
def __init__(self, directories=True): def __init__(self, directories=True):
self.directories = directories self.directories = directories
def __call__(self, prefix, **kwargs): def __call__(self, prefix, **kwargs):
"""only called on non option completions""" """only called on non option completions"""
if os.path.sep in prefix[1:]: # if os.path.sep in prefix[1:]:
prefix_dir = len(os.path.dirname(prefix) + os.path.sep) prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
else: else:
prefix_dir = 0 prefix_dir = 0
completion = [] completion = []
globbed = [] globbed = []
if '*' not in prefix and '?' not in prefix: if '*' not in prefix and '?' not in prefix:
if prefix[-1] == os.path.sep: # we are on unix, otherwise no bash # we are on unix, otherwise no bash
if not prefix or prefix[-1] == os.path.sep:
globbed.extend(glob(prefix + '.*')) globbed.extend(glob(prefix + '.*'))
prefix += '*' prefix += '*'
globbed.extend(glob(prefix)) globbed.extend(glob(prefix))
@ -96,7 +99,8 @@ def __call__(self, prefix, **kwargs):
filescompleter = FastFilesCompleter() filescompleter = FastFilesCompleter()
def try_argcomplete(parser): def try_argcomplete(parser):
argcomplete.autocomplete(parser) argcomplete.autocomplete(parser, always_complete_options=False)
else: else:
def try_argcomplete(parser): pass def try_argcomplete(parser):
pass
filescompleter = None filescompleter = None

View File

@ -1,4 +1,5 @@
""" python inspection/code generation API """ """ python inspection/code generation API """
from __future__ import absolute_import, division, print_function
from .code import Code # noqa from .code import Code # noqa
from .code import ExceptionInfo # noqa from .code import ExceptionInfo # noqa
from .code import Frame # noqa from .code import Frame # noqa

View File

@ -2,8 +2,10 @@
# CHANGES: # CHANGES:
# - some_str is replaced, trying to create unicode strings # - some_str is replaced, trying to create unicode strings
# #
from __future__ import absolute_import, division, print_function
import types import types
def format_exception_only(etype, value): def format_exception_only(etype, value):
"""Format the exception part of a traceback. """Format the exception part of a traceback.
@ -29,7 +31,7 @@ def format_exception_only(etype, value):
# would throw another exception and mask the original problem. # would throw another exception and mask the original problem.
if (isinstance(etype, BaseException) or if (isinstance(etype, BaseException) or
isinstance(etype, types.InstanceType) or isinstance(etype, types.InstanceType) or
etype is None or type(etype) is str): etype is None or type(etype) is str):
return [_format_final_exc_line(etype, value)] return [_format_final_exc_line(etype, value)]
stype = etype.__name__ stype = etype.__name__
@ -61,6 +63,7 @@ def format_exception_only(etype, value):
lines.append(_format_final_exc_line(stype, value)) lines.append(_format_final_exc_line(stype, value))
return lines return lines
def _format_final_exc_line(etype, value): def _format_final_exc_line(etype, value):
"""Return a list of a single line -- normal case for format_exception_only""" """Return a list of a single line -- normal case for format_exception_only"""
valuestr = _some_str(value) valuestr = _some_str(value)
@ -70,6 +73,7 @@ def _format_final_exc_line(etype, value):
line = "%s: %s\n" % (etype, valuestr) line = "%s: %s\n" % (etype, valuestr)
return line return line
def _some_str(value): def _some_str(value):
try: try:
return unicode(value) return unicode(value)

View File

@ -1,14 +1,16 @@
from __future__ import absolute_import, division, print_function
import sys import sys
from inspect import CO_VARARGS, CO_VARKEYWORDS from inspect import CO_VARARGS, CO_VARKEYWORDS
import re import re
from weakref import ref from weakref import ref
from _pytest.compat import _PY2, _PY3, PY35, safe_str
import py import py
builtin_repr = repr builtin_repr = repr
reprlib = py.builtin._tryimport('repr', 'reprlib') reprlib = py.builtin._tryimport('repr', 'reprlib')
if sys.version_info[0] >= 3: if _PY3:
from traceback import format_exception_only from traceback import format_exception_only
else: else:
from ._py2traceback import format_exception_only from ._py2traceback import format_exception_only
@ -16,6 +18,7 @@
class Code(object): class Code(object):
""" wrapper around Python code objects """ """ wrapper around Python code objects """
def __init__(self, rawcode): def __init__(self, rawcode):
if not hasattr(rawcode, "co_filename"): if not hasattr(rawcode, "co_filename"):
rawcode = getrawcode(rawcode) rawcode = getrawcode(rawcode)
@ -24,7 +27,7 @@ def __init__(self, rawcode):
self.firstlineno = rawcode.co_firstlineno - 1 self.firstlineno = rawcode.co_firstlineno - 1
self.name = rawcode.co_name self.name = rawcode.co_name
except AttributeError: except AttributeError:
raise TypeError("not a code object: %r" %(rawcode,)) raise TypeError("not a code object: %r" % (rawcode,))
self.raw = rawcode self.raw = rawcode
def __eq__(self, other): def __eq__(self, other):
@ -80,6 +83,7 @@ def getargs(self, var=False):
argcount += raw.co_flags & CO_VARKEYWORDS argcount += raw.co_flags & CO_VARKEYWORDS
return raw.co_varnames[:argcount] return raw.co_varnames[:argcount]
class Frame(object): class Frame(object):
"""Wrapper around a Python frame holding f_locals and f_globals """Wrapper around a Python frame holding f_locals and f_globals
in which expressions can be evaluated.""" in which expressions can be evaluated."""
@ -117,7 +121,7 @@ def exec_(self, code, **vars):
""" """
f_locals = self.f_locals.copy() f_locals = self.f_locals.copy()
f_locals.update(vars) f_locals.update(vars)
py.builtin.exec_(code, self.f_globals, f_locals ) py.builtin.exec_(code, self.f_globals, f_locals)
def repr(self, object): def repr(self, object):
""" return a 'safe' (non-recursive, one-line) string repr for 'object' """ return a 'safe' (non-recursive, one-line) string repr for 'object'
@ -141,6 +145,7 @@ def getargs(self, var=False):
pass # this can occur when using Psyco pass # this can occur when using Psyco
return retval return retval
class TracebackEntry(object): class TracebackEntry(object):
""" a single entry in a traceback """ """ a single entry in a traceback """
@ -166,7 +171,7 @@ def relline(self):
return self.lineno - self.frame.code.firstlineno return self.lineno - self.frame.code.firstlineno
def __repr__(self): def __repr__(self):
return "<TracebackEntry %s:%d>" %(self.frame.code.path, self.lineno+1) return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
@property @property
def statement(self): def statement(self):
@ -245,19 +250,21 @@ def __str__(self):
line = str(self.statement).lstrip() line = str(self.statement).lstrip()
except KeyboardInterrupt: except KeyboardInterrupt:
raise raise
except: except: # noqa
line = "???" line = "???"
return " File %r:%d in %s\n %s\n" %(fn, self.lineno+1, name, line) return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line)
def name(self): def name(self):
return self.frame.code.raw.co_name return self.frame.code.raw.co_name
name = property(name, None, None, "co_name of underlaying code") name = property(name, None, None, "co_name of underlaying code")
class Traceback(list): class Traceback(list):
""" Traceback objects encapsulate and offer higher level """ Traceback objects encapsulate and offer higher level
access to Traceback entries. access to Traceback entries.
""" """
Entry = TracebackEntry Entry = TracebackEntry
def __init__(self, tb, excinfo=None): def __init__(self, tb, excinfo=None):
""" initialize from given python traceback object and ExceptionInfo """ """ initialize from given python traceback object and ExceptionInfo """
self._excinfo = excinfo self._excinfo = excinfo
@ -287,7 +294,7 @@ def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None):
(excludepath is None or not hasattr(codepath, 'relto') or (excludepath is None or not hasattr(codepath, 'relto') or
not codepath.relto(excludepath)) and not codepath.relto(excludepath)) and
(lineno is None or x.lineno == lineno) and (lineno is None or x.lineno == lineno) and
(firstlineno is None or x.frame.code.firstlineno == firstlineno)): (firstlineno is None or x.frame.code.firstlineno == firstlineno)):
return Traceback(x._rawentry, self._excinfo) return Traceback(x._rawentry, self._excinfo)
return self return self
@ -313,7 +320,7 @@ def getcrashentry(self):
""" return last non-hidden traceback entry that lead """ return last non-hidden traceback entry that lead
to the exception of a traceback. to the exception of a traceback.
""" """
for i in range(-1, -len(self)-1, -1): for i in range(-1, -len(self) - 1, -1):
entry = self[i] entry = self[i]
if not entry.ishidden(): if not entry.ishidden():
return entry return entry
@ -328,30 +335,33 @@ def recursionindex(self):
# id for the code.raw is needed to work around # id for the code.raw is needed to work around
# the strange metaprogramming in the decorator lib from pypi # the strange metaprogramming in the decorator lib from pypi
# which generates code objects that have hash/value equality # which generates code objects that have hash/value equality
#XXX needs a test # XXX needs a test
key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
#print "checking for recursion at", key # print "checking for recursion at", key
l = cache.setdefault(key, []) values = cache.setdefault(key, [])
if l: if values:
f = entry.frame f = entry.frame
loc = f.f_locals loc = f.f_locals
for otherloc in l: for otherloc in values:
if f.is_true(f.eval(co_equal, if f.is_true(f.eval(co_equal,
__recursioncache_locals_1=loc, __recursioncache_locals_1=loc,
__recursioncache_locals_2=otherloc)): __recursioncache_locals_2=otherloc)):
return i return i
l.append(entry.frame.f_locals) values.append(entry.frame.f_locals)
return None return None
co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2', co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2',
'?', 'eval') '?', 'eval')
class ExceptionInfo(object): class ExceptionInfo(object):
""" wraps sys.exc_info() objects and offers """ wraps sys.exc_info() objects and offers
help for navigating the traceback. help for navigating the traceback.
""" """
_striptext = '' _striptext = ''
_assert_start_repr = "AssertionError(u\'assert " if _PY2 else "AssertionError(\'assert "
def __init__(self, tup=None, exprinfo=None): def __init__(self, tup=None, exprinfo=None):
import _pytest._code import _pytest._code
if tup is None: if tup is None:
@ -359,8 +369,8 @@ def __init__(self, tup=None, exprinfo=None):
if exprinfo is None and isinstance(tup[1], AssertionError): if exprinfo is None and isinstance(tup[1], AssertionError):
exprinfo = getattr(tup[1], 'msg', None) exprinfo = getattr(tup[1], 'msg', None)
if exprinfo is None: if exprinfo is None:
exprinfo = py._builtin._totext(tup[1]) exprinfo = py.io.saferepr(tup[1])
if exprinfo and exprinfo.startswith('assert '): if exprinfo and exprinfo.startswith(self._assert_start_repr):
self._striptext = 'AssertionError: ' self._striptext = 'AssertionError: '
self._excinfo = tup self._excinfo = tup
#: the exception class #: the exception class
@ -401,10 +411,10 @@ def _getreprcrash(self):
exconly = self.exconly(tryshort=True) exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry() entry = self.traceback.getcrashentry()
path, lineno = entry.frame.code.raw.co_filename, entry.lineno path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno+1, exconly) return ReprFileLocation(path, lineno + 1, exconly)
def getrepr(self, showlocals=False, style="long", def getrepr(self, showlocals=False, style="long",
abspath=False, tbfilter=True, funcargs=False): abspath=False, tbfilter=True, funcargs=False):
""" return str()able representation of this exception info. """ return str()able representation of this exception info.
showlocals: show locals per traceback entry showlocals: show locals per traceback entry
style: long|short|no|native traceback style style: long|short|no|native traceback style
@ -421,7 +431,7 @@ def getrepr(self, showlocals=False, style="long",
)), self._getreprcrash()) )), self._getreprcrash())
fmt = FormattedExcinfo(showlocals=showlocals, style=style, fmt = FormattedExcinfo(showlocals=showlocals, style=style,
abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) abspath=abspath, tbfilter=tbfilter, funcargs=funcargs)
return fmt.repr_excinfo(self) return fmt.repr_excinfo(self)
def __str__(self): def __str__(self):
@ -465,15 +475,15 @@ def __init__(self, showlocals=False, style="long", abspath=True, tbfilter=True,
def _getindent(self, source): def _getindent(self, source):
# figure out indent for given source # figure out indent for given source
try: try:
s = str(source.getstatement(len(source)-1)) s = str(source.getstatement(len(source) - 1))
except KeyboardInterrupt: except KeyboardInterrupt:
raise raise
except: except: # noqa
try: try:
s = str(source[-1]) s = str(source[-1])
except KeyboardInterrupt: except KeyboardInterrupt:
raise raise
except: except: # noqa
return 0 return 0
return 4 + (len(s) - len(s.lstrip())) return 4 + (len(s) - len(s.lstrip()))
@ -509,7 +519,7 @@ def get_source(self, source, line_index=-1, excinfo=None, short=False):
for line in source.lines[:line_index]: for line in source.lines[:line_index]:
lines.append(space_prefix + line) lines.append(space_prefix + line)
lines.append(self.flow_marker + " " + source.lines[line_index]) lines.append(self.flow_marker + " " + source.lines[line_index])
for line in source.lines[line_index+1:]: for line in source.lines[line_index + 1:]:
lines.append(space_prefix + line) lines.append(space_prefix + line)
if excinfo is not None: if excinfo is not None:
indent = 4 if short else self._getindent(source) indent = 4 if short else self._getindent(source)
@ -542,10 +552,10 @@ def repr_locals(self, locals):
# _repr() function, which is only reprlib.Repr in # _repr() function, which is only reprlib.Repr in
# disguise, so is very configurable. # disguise, so is very configurable.
str_repr = self._saferepr(value) str_repr = self._saferepr(value)
#if len(str_repr) < 70 or not isinstance(value, # if len(str_repr) < 70 or not isinstance(value,
# (list, tuple, dict)): # (list, tuple, dict)):
lines.append("%-10s = %s" %(name, str_repr)) lines.append("%-10s = %s" % (name, str_repr))
#else: # else:
# self._line("%-10s =\\" % (name,)) # self._line("%-10s =\\" % (name,))
# # XXX # # XXX
# py.std.pprint.pprint(value, stream=self.excinfowriter) # py.std.pprint.pprint(value, stream=self.excinfowriter)
@ -571,14 +581,14 @@ def repr_traceback_entry(self, entry, excinfo=None):
s = self.get_source(source, line_index, excinfo, short=short) s = self.get_source(source, line_index, excinfo, short=short)
lines.extend(s) lines.extend(s)
if short: if short:
message = "in %s" %(entry.name) message = "in %s" % (entry.name)
else: else:
message = excinfo and excinfo.typename or "" message = excinfo and excinfo.typename or ""
path = self._makepath(entry.path) path = self._makepath(entry.path)
filelocrepr = ReprFileLocation(path, entry.lineno+1, message) filelocrepr = ReprFileLocation(path, entry.lineno + 1, message)
localsrepr = None localsrepr = None
if not short: if not short:
localsrepr = self.repr_locals(entry.locals) localsrepr = self.repr_locals(entry.locals)
return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style) return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style)
if excinfo: if excinfo:
lines.extend(self.get_exconly(excinfo, indent=4)) lines.extend(self.get_exconly(excinfo, indent=4))
@ -598,24 +608,54 @@ def repr_traceback(self, excinfo):
traceback = excinfo.traceback traceback = excinfo.traceback
if self.tbfilter: if self.tbfilter:
traceback = traceback.filter() traceback = traceback.filter()
recursionindex = None
if is_recursion_error(excinfo): if is_recursion_error(excinfo):
recursionindex = traceback.recursionindex() traceback, extraline = self._truncate_recursive_traceback(traceback)
else:
extraline = None
last = traceback[-1] last = traceback[-1]
entries = [] entries = []
extraline = None
for index, entry in enumerate(traceback): for index, entry in enumerate(traceback):
einfo = (last == entry) and excinfo or None einfo = (last == entry) and excinfo or None
reprentry = self.repr_traceback_entry(entry, einfo) reprentry = self.repr_traceback_entry(entry, einfo)
entries.append(reprentry) entries.append(reprentry)
if index == recursionindex:
extraline = "!!! Recursion detected (same locals & position)"
break
return ReprTraceback(entries, extraline, style=self.style) return ReprTraceback(entries, extraline, style=self.style)
def _truncate_recursive_traceback(self, traceback):
"""
Truncate the given recursive traceback trying to find the starting point
of the recursion.
The detection is done by going through each traceback entry and finding the
point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``.
Handle the situation where the recursion process might raise an exception (for example
comparing numpy arrays using equality raises a TypeError), in which case we do our best to
warn the user of the error and show a limited traceback.
"""
try:
recursionindex = traceback.recursionindex()
except Exception as e:
max_frames = 10
extraline = (
'!!! Recursion error detected, but an error occurred locating the origin of recursion.\n'
' The following exception happened when comparing locals in the stack frame:\n'
' {exc_type}: {exc_msg}\n'
' Displaying first and last {max_frames} stack frames out of {total}.'
).format(exc_type=type(e).__name__, exc_msg=safe_str(e), max_frames=max_frames, total=len(traceback))
traceback = traceback[:max_frames] + traceback[-max_frames:]
else:
if recursionindex is not None:
extraline = "!!! Recursion detected (same locals & position)"
traceback = traceback[:recursionindex + 1]
else:
extraline = None
return traceback, extraline
def repr_excinfo(self, excinfo): def repr_excinfo(self, excinfo):
if sys.version_info[0] < 3: if _PY2:
reprtraceback = self.repr_traceback(excinfo) reprtraceback = self.repr_traceback(excinfo)
reprcrash = excinfo._getreprcrash() reprcrash = excinfo._getreprcrash()
@ -639,7 +679,7 @@ def repr_excinfo(self, excinfo):
e = e.__cause__ e = e.__cause__
excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None
descr = 'The above exception was the direct cause of the following exception:' descr = 'The above exception was the direct cause of the following exception:'
elif e.__context__ is not None: elif (e.__context__ is not None and not e.__suppress_context__):
e = e.__context__ e = e.__context__
excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None
descr = 'During handling of the above exception, another exception occurred:' descr = 'During handling of the above exception, another exception occurred:'
@ -652,7 +692,7 @@ def repr_excinfo(self, excinfo):
class TerminalRepr(object): class TerminalRepr(object):
def __str__(self): def __str__(self):
s = self.__unicode__() s = self.__unicode__()
if sys.version_info[0] < 3: if _PY2:
s = s.encode('utf-8') s = s.encode('utf-8')
return s return s
@ -665,7 +705,7 @@ def __unicode__(self):
return io.getvalue().strip() return io.getvalue().strip()
def __repr__(self): def __repr__(self):
return "<%s instance at %0x>" %(self.__class__, id(self)) return "<%s instance at %0x>" % (self.__class__, id(self))
class ExceptionRepr(TerminalRepr): class ExceptionRepr(TerminalRepr):
@ -709,6 +749,7 @@ def toterminal(self, tw):
self.reprtraceback.toterminal(tw) self.reprtraceback.toterminal(tw)
super(ReprExceptionInfo, self).toterminal(tw) super(ReprExceptionInfo, self).toterminal(tw)
class ReprTraceback(TerminalRepr): class ReprTraceback(TerminalRepr):
entrysep = "_ " entrysep = "_ "
@ -724,7 +765,7 @@ def toterminal(self, tw):
tw.line("") tw.line("")
entry.toterminal(tw) entry.toterminal(tw)
if i < len(self.reprentries) - 1: if i < len(self.reprentries) - 1:
next_entry = self.reprentries[i+1] next_entry = self.reprentries[i + 1]
if entry.style == "long" or \ if entry.style == "long" or \
entry.style == "short" and next_entry.style == "long": entry.style == "short" and next_entry.style == "long":
tw.sep(self.entrysep) tw.sep(self.entrysep)
@ -732,12 +773,14 @@ def toterminal(self, tw):
if self.extraline: if self.extraline:
tw.line(self.extraline) tw.line(self.extraline)
class ReprTracebackNative(ReprTraceback): class ReprTracebackNative(ReprTraceback):
def __init__(self, tblines): def __init__(self, tblines):
self.style = "native" self.style = "native"
self.reprentries = [ReprEntryNative(tblines)] self.reprentries = [ReprEntryNative(tblines)]
self.extraline = None self.extraline = None
class ReprEntryNative(TerminalRepr): class ReprEntryNative(TerminalRepr):
style = "native" style = "native"
@ -747,6 +790,7 @@ def __init__(self, tblines):
def toterminal(self, tw): def toterminal(self, tw):
tw.write("".join(self.lines)) tw.write("".join(self.lines))
class ReprEntry(TerminalRepr): class ReprEntry(TerminalRepr):
localssep = "_ " localssep = "_ "
@ -763,7 +807,7 @@ def toterminal(self, tw):
for line in self.lines: for line in self.lines:
red = line.startswith("E ") red = line.startswith("E ")
tw.line(line, bold=True, red=red) tw.line(line, bold=True, red=red)
#tw.line("") # tw.line("")
return return
if self.reprfuncargs: if self.reprfuncargs:
self.reprfuncargs.toterminal(tw) self.reprfuncargs.toterminal(tw)
@ -771,7 +815,7 @@ def toterminal(self, tw):
red = line.startswith("E ") red = line.startswith("E ")
tw.line(line, bold=True, red=red) tw.line(line, bold=True, red=red)
if self.reprlocals: if self.reprlocals:
#tw.sep(self.localssep, "Locals") # tw.sep(self.localssep, "Locals")
tw.line("") tw.line("")
self.reprlocals.toterminal(tw) self.reprlocals.toterminal(tw)
if self.reprfileloc: if self.reprfileloc:
@ -784,6 +828,7 @@ def __str__(self):
self.reprlocals, self.reprlocals,
self.reprfileloc) self.reprfileloc)
class ReprFileLocation(TerminalRepr): class ReprFileLocation(TerminalRepr):
def __init__(self, path, lineno, message): def __init__(self, path, lineno, message):
self.path = str(path) self.path = str(path)
@ -800,6 +845,7 @@ def toterminal(self, tw):
tw.write(self.path, bold=True, red=True) tw.write(self.path, bold=True, red=True)
tw.line(":%s: %s" % (self.lineno, msg)) tw.line(":%s: %s" % (self.lineno, msg))
class ReprLocals(TerminalRepr): class ReprLocals(TerminalRepr):
def __init__(self, lines): def __init__(self, lines):
self.lines = lines self.lines = lines
@ -808,6 +854,7 @@ def toterminal(self, tw):
for line in self.lines: for line in self.lines:
tw.line(line) tw.line(line)
class ReprFuncArgs(TerminalRepr): class ReprFuncArgs(TerminalRepr):
def __init__(self, args): def __init__(self, args):
self.args = args self.args = args
@ -816,11 +863,11 @@ def toterminal(self, tw):
if self.args: if self.args:
linesofar = "" linesofar = ""
for name, value in self.args: for name, value in self.args:
ns = "%s = %s" %(name, value) ns = "%s = %s" % (safe_str(name), safe_str(value))
if len(ns) + len(linesofar) + 2 > tw.fullwidth: if len(ns) + len(linesofar) + 2 > tw.fullwidth:
if linesofar: if linesofar:
tw.line(linesofar) tw.line(linesofar)
linesofar = ns linesofar = ns
else: else:
if linesofar: if linesofar:
linesofar += ", " + ns linesofar += ", " + ns
@ -848,7 +895,7 @@ def getrawcode(obj, trycall=True):
return obj return obj
if sys.version_info[:2] >= (3, 5): # RecursionError introduced in 3.5 if PY35: # RecursionError introduced in 3.5
def is_recursion_error(excinfo): def is_recursion_error(excinfo):
return excinfo.errisinstance(RecursionError) # noqa return excinfo.errisinstance(RecursionError) # noqa
else: else:

View File

@ -1,8 +1,9 @@
from __future__ import generators from __future__ import absolute_import, division, generators, print_function
from bisect import bisect_right from bisect import bisect_right
import sys import sys
import inspect, tokenize import inspect
import tokenize
import py import py
cpy_compile = compile cpy_compile = compile
@ -19,6 +20,7 @@ class Source(object):
possibly deindenting it. possibly deindenting it.
""" """
_compilecounter = 0 _compilecounter = 0
def __init__(self, *parts, **kwargs): def __init__(self, *parts, **kwargs):
self.lines = lines = [] self.lines = lines = []
de = kwargs.get('deindent', True) de = kwargs.get('deindent', True)
@ -73,7 +75,7 @@ def strip(self):
start, end = 0, len(self) start, end = 0, len(self)
while start < end and not self.lines[start].strip(): while start < end and not self.lines[start].strip():
start += 1 start += 1
while end > start and not self.lines[end-1].strip(): while end > start and not self.lines[end - 1].strip():
end -= 1 end -= 1
source = Source() source = Source()
source.lines[:] = self.lines[start:end] source.lines[:] = self.lines[start:end]
@ -86,8 +88,8 @@ def putaround(self, before='', after='', indent=' ' * 4):
before = Source(before) before = Source(before)
after = Source(after) after = Source(after)
newsource = Source() newsource = Source()
lines = [ (indent + line) for line in self.lines] lines = [(indent + line) for line in self.lines]
newsource.lines = before.lines + lines + after.lines newsource.lines = before.lines + lines + after.lines
return newsource return newsource
def indent(self, indent=' ' * 4): def indent(self, indent=' ' * 4):
@ -95,7 +97,7 @@ def indent(self, indent=' ' * 4):
all lines indented by the given indent-string. all lines indented by the given indent-string.
""" """
newsource = Source() newsource = Source()
newsource.lines = [(indent+line) for line in self.lines] newsource.lines = [(indent + line) for line in self.lines]
return newsource return newsource
def getstatement(self, lineno, assertion=False): def getstatement(self, lineno, assertion=False):
@ -134,7 +136,8 @@ def isparseable(self, deindent=True):
try: try:
import parser import parser
except ImportError: except ImportError:
syntax_checker = lambda x: compile(x, 'asd', 'exec') def syntax_checker(x):
return compile(x, 'asd', 'exec')
else: else:
syntax_checker = parser.suite syntax_checker = parser.suite
@ -143,8 +146,8 @@ def isparseable(self, deindent=True):
else: else:
source = str(self) source = str(self)
try: try:
#compile(source+'\n', "x", "exec") # compile(source+'\n', "x", "exec")
syntax_checker(source+'\n') syntax_checker(source + '\n')
except KeyboardInterrupt: except KeyboardInterrupt:
raise raise
except Exception: except Exception:
@ -164,8 +167,8 @@ def compile(self, filename=None, mode='exec',
""" """
if not filename or py.path.local(filename).check(file=0): if not filename or py.path.local(filename).check(file=0):
if _genframe is None: if _genframe is None:
_genframe = sys._getframe(1) # the caller _genframe = sys._getframe(1) # the caller
fn,lineno = _genframe.f_code.co_filename, _genframe.f_lineno fn, lineno = _genframe.f_code.co_filename, _genframe.f_lineno
base = "<%d-codegen " % self._compilecounter base = "<%d-codegen " % self._compilecounter
self.__class__._compilecounter += 1 self.__class__._compilecounter += 1
if not filename: if not filename:
@ -180,7 +183,7 @@ def compile(self, filename=None, mode='exec',
# re-represent syntax errors from parsing python strings # re-represent syntax errors from parsing python strings
msglines = self.lines[:ex.lineno] msglines = self.lines[:ex.lineno]
if ex.offset: if ex.offset:
msglines.append(" "*ex.offset + '^') msglines.append(" " * ex.offset + '^')
msglines.append("(code was compiled probably from here: %s)" % filename) msglines.append("(code was compiled probably from here: %s)" % filename)
newex = SyntaxError('\n'.join(msglines)) newex = SyntaxError('\n'.join(msglines))
newex.offset = ex.offset newex.offset = ex.offset
@ -198,8 +201,8 @@ def compile(self, filename=None, mode='exec',
# public API shortcut functions # public API shortcut functions
# #
def compile_(source, filename=None, mode='exec', flags=
generators.compiler_flag, dont_inherit=0): def compile_(source, filename=None, mode='exec', flags=generators.compiler_flag, dont_inherit=0):
""" compile the given source to a raw code object, """ compile the given source to a raw code object,
and maintain an internal cache which allows later and maintain an internal cache which allows later
retrieval of the source code for the code object retrieval of the source code for the code object
@ -208,7 +211,7 @@ def compile_(source, filename=None, mode='exec', flags=
if _ast is not None and isinstance(source, _ast.AST): if _ast is not None and isinstance(source, _ast.AST):
# XXX should Source support having AST? # XXX should Source support having AST?
return cpy_compile(source, filename, mode, flags, dont_inherit) return cpy_compile(source, filename, mode, flags, dont_inherit)
_genframe = sys._getframe(1) # the caller _genframe = sys._getframe(1) # the caller
s = Source(source) s = Source(source)
co = s.compile(filename, mode, flags, _genframe=_genframe) co = s.compile(filename, mode, flags, _genframe=_genframe)
return co return co
@ -245,12 +248,13 @@ def getfslineno(obj):
# helper functions # helper functions
# #
def findsource(obj): def findsource(obj):
try: try:
sourcelines, lineno = py.std.inspect.findsource(obj) sourcelines, lineno = py.std.inspect.findsource(obj)
except py.builtin._sysex: except py.builtin._sysex:
raise raise
except: except: # noqa
return None, -1 return None, -1
source = Source() source = Source()
source.lines = [line.rstrip() for line in sourcelines] source.lines = [line.rstrip() for line in sourcelines]
@ -274,7 +278,7 @@ def deindent(lines, offset=None):
line = line.expandtabs() line = line.expandtabs()
s = line.lstrip() s = line.lstrip()
if s: if s:
offset = len(line)-len(s) offset = len(line) - len(s)
break break
else: else:
offset = 0 offset = 0
@ -293,11 +297,11 @@ def readline_generator(lines):
try: try:
for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(lambda: next(it)): for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(lambda: next(it)):
if sline > len(lines): if sline > len(lines):
break # End of input reached break # End of input reached
if sline > len(newlines): if sline > len(newlines):
line = lines[sline - 1].expandtabs() line = lines[sline - 1].expandtabs()
if line.lstrip() and line[:offset].isspace(): if line.lstrip() and line[:offset].isspace():
line = line[offset:] # Deindent line = line[offset:] # Deindent
newlines.append(line) newlines.append(line)
for i in range(sline, eline): for i in range(sline, eline):
@ -315,29 +319,29 @@ def get_statement_startend2(lineno, node):
import ast import ast
# flatten all statements and except handlers into one lineno-list # flatten all statements and except handlers into one lineno-list
# AST's line numbers start indexing at 1 # AST's line numbers start indexing at 1
l = [] values = []
for x in ast.walk(node): for x in ast.walk(node):
if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler): if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler):
l.append(x.lineno - 1) values.append(x.lineno - 1)
for name in "finalbody", "orelse": for name in "finalbody", "orelse":
val = getattr(x, name, None) val = getattr(x, name, None)
if val: if val:
# treat the finally/orelse part as its own statement # treat the finally/orelse part as its own statement
l.append(val[0].lineno - 1 - 1) values.append(val[0].lineno - 1 - 1)
l.sort() values.sort()
insert_index = bisect_right(l, lineno) insert_index = bisect_right(values, lineno)
start = l[insert_index - 1] start = values[insert_index - 1]
if insert_index >= len(l): if insert_index >= len(values):
end = None end = None
else: else:
end = l[insert_index] end = values[insert_index]
return start, end return start, end
def getstatementrange_ast(lineno, source, assertion=False, astnode=None): def getstatementrange_ast(lineno, source, assertion=False, astnode=None):
if astnode is None: if astnode is None:
content = str(source) content = str(source)
if sys.version_info < (2,7): if sys.version_info < (2, 7):
content += "\n" content += "\n"
try: try:
astnode = compile(content, "source", "exec", 1024) # 1024 for AST astnode = compile(content, "source", "exec", 1024) # 1024 for AST
@ -393,7 +397,7 @@ def getstatementrange_old(lineno, source, assertion=False):
raise IndexError("likely a subclass") raise IndexError("likely a subclass")
if "assert" not in line and "raise" not in line: if "assert" not in line and "raise" not in line:
continue continue
trylines = source.lines[start:lineno+1] trylines = source.lines[start:lineno + 1]
# quick hack to prepare parsing an indented line with # quick hack to prepare parsing an indented line with
# compile_command() (which errors on "return" outside defs) # compile_command() (which errors on "return" outside defs)
trylines.insert(0, 'def xxx():') trylines.insert(0, 'def xxx():')
@ -405,10 +409,8 @@ def getstatementrange_old(lineno, source, assertion=False):
continue continue
# 2. find the end of the statement # 2. find the end of the statement
for end in range(lineno+1, len(source)+1): for end in range(lineno + 1, len(source) + 1):
trysource = source[start:end] trysource = source[start:end]
if trysource.isparseable(): if trysource.isparseable():
return start, end return start, end
raise SyntaxError("no valid source range around line %d " % (lineno,)) raise SyntaxError("no valid source range around line %d " % (lineno,))

View File

@ -2,7 +2,7 @@
imports symbols from vendored "pluggy" if available, otherwise imports symbols from vendored "pluggy" if available, otherwise
falls back to importing "pluggy" from the default namespace. falls back to importing "pluggy" from the default namespace.
""" """
from __future__ import absolute_import, division, print_function
try: try:
from _pytest.vendored_packages.pluggy import * # noqa from _pytest.vendored_packages.pluggy import * # noqa
from _pytest.vendored_packages.pluggy import __version__ # noqa from _pytest.vendored_packages.pluggy import __version__ # noqa

View File

@ -1,12 +1,13 @@
""" """
support for presenting detailed information in failing assertions. support for presenting detailed information in failing assertions.
""" """
from __future__ import absolute_import, division, print_function
import py import py
import os
import sys import sys
from _pytest.assertion import util from _pytest.assertion import util
from _pytest.assertion import rewrite from _pytest.assertion import rewrite
from _pytest.assertion import truncate
def pytest_addoption(parser): def pytest_addoption(parser):
@ -24,10 +25,6 @@ def pytest_addoption(parser):
expression information.""") expression information.""")
def pytest_namespace():
return {'register_assert_rewrite': register_assert_rewrite}
def register_assert_rewrite(*names): def register_assert_rewrite(*names):
"""Register one or more module names to be rewritten on import. """Register one or more module names to be rewritten on import.
@ -100,12 +97,6 @@ def pytest_collection(session):
assertstate.hook.set_session(session) assertstate.hook.set_session(session)
def _running_on_ci():
"""Check if we're currently running on a CI system."""
env_vars = ['CI', 'BUILD_NUMBER']
return any(var in os.environ for var in env_vars)
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
"""Setup the pytest_assertrepr_compare hook """Setup the pytest_assertrepr_compare hook
@ -119,8 +110,8 @@ def callbinrepr(op, left, right):
This uses the first result from the hook and then ensures the This uses the first result from the hook and then ensures the
following: following:
* Overly verbose explanations are dropped unless -vv was used or * Overly verbose explanations are truncated unless configured otherwise
running on a CI. (eg. if running in verbose mode).
* Embedded newlines are escaped to help util.format_explanation() * Embedded newlines are escaped to help util.format_explanation()
later. later.
* If the rewrite mode is used embedded %-characters are replaced * If the rewrite mode is used embedded %-characters are replaced
@ -133,14 +124,7 @@ def callbinrepr(op, left, right):
config=item.config, op=op, left=left, right=right) config=item.config, op=op, left=left, right=right)
for new_expl in hook_result: for new_expl in hook_result:
if new_expl: if new_expl:
if (sum(len(p) for p in new_expl[1:]) > 80*8 and new_expl = truncate.truncate_if_required(new_expl, item)
item.config.option.verbose < 2 and
not _running_on_ci()):
show_max = 10
truncated_lines = len(new_expl) - show_max
new_expl[show_max:] = [py.builtin._totext(
'Detailed information truncated (%d more lines)'
', use "-vv" to show' % truncated_lines)]
new_expl = [line.replace("\n", "\\n") for line in new_expl] new_expl = [line.replace("\n", "\\n") for line in new_expl]
res = py.builtin._totext("\n~").join(new_expl) res = py.builtin._totext("\n~").join(new_expl)
if item.config.getvalue("assertmode") == "rewrite": if item.config.getvalue("assertmode") == "rewrite":

View File

@ -1,5 +1,5 @@
"""Rewrite assertion AST to produce nice error messages""" """Rewrite assertion AST to produce nice error messages"""
from __future__ import absolute_import, division, print_function
import ast import ast
import _ast import _ast
import errno import errno
@ -11,7 +11,6 @@
import struct import struct
import sys import sys
import types import types
from fnmatch import fnmatch
import py import py
from _pytest.assertion import util from _pytest.assertion import util
@ -37,10 +36,11 @@
REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2) REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2)
ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3 ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3
if sys.version_info >= (3,5): if sys.version_info >= (3, 5):
ast_Call = ast.Call ast_Call = ast.Call
else: else:
ast_Call = lambda a,b,c: ast.Call(a, b, c, None, None) def ast_Call(a, b, c):
return ast.Call(a, b, c, None, None)
class AssertionRewritingHook(object): class AssertionRewritingHook(object):
@ -163,11 +163,7 @@ def _should_rewrite(self, name, fn_pypath, state):
# modules not passed explicitly on the command line are only # modules not passed explicitly on the command line are only
# rewritten if they match the naming convention for test files # rewritten if they match the naming convention for test files
for pat in self.fnpats: for pat in self.fnpats:
# use fnmatch instead of fn_pypath.fnmatch because the if fn_pypath.fnmatch(pat):
# latter might trigger an import to fnmatch.fnmatch
# internally, which would cause this method to be
# called recursively
if fnmatch(fn_pypath.basename, pat):
state.trace("matched test file %r" % (fn,)) state.trace("matched test file %r" % (fn,))
return True return True
@ -214,13 +210,12 @@ def load_module(self, name):
mod.__cached__ = pyc mod.__cached__ = pyc
mod.__loader__ = self mod.__loader__ = self
py.builtin.exec_(co, mod.__dict__) py.builtin.exec_(co, mod.__dict__)
except: except: # noqa
del sys.modules[name] if name in sys.modules:
del sys.modules[name]
raise raise
return sys.modules[name] return sys.modules[name]
def is_package(self, name): def is_package(self, name):
try: try:
fd, fn, desc = imp.find_module(name) fd, fn, desc = imp.find_module(name)
@ -265,7 +260,7 @@ def _write_pyc(state, co, source_stat, pyc):
fp = open(pyc, "wb") fp = open(pyc, "wb")
except IOError: except IOError:
err = sys.exc_info()[1].errno err = sys.exc_info()[1].errno
state.trace("error writing pyc file at %s: errno=%s" %(pyc, err)) state.trace("error writing pyc file at %s: errno=%s" % (pyc, err))
# we ignore any failure to write the cache file # we ignore any failure to write the cache file
# there are many reasons, permission-denied, __pycache__ being a # there are many reasons, permission-denied, __pycache__ being a
# file etc. # file etc.
@ -287,6 +282,7 @@ def _write_pyc(state, co, source_stat, pyc):
cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+") cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+")
BOM_UTF8 = '\xef\xbb\xbf' BOM_UTF8 = '\xef\xbb\xbf'
def _rewrite_test(config, fn): def _rewrite_test(config, fn):
"""Try to read and rewrite *fn* and return the code object.""" """Try to read and rewrite *fn* and return the code object."""
state = config._assertstate state = config._assertstate
@ -311,7 +307,7 @@ def _rewrite_test(config, fn):
end2 = source.find("\n", end1 + 1) end2 = source.find("\n", end1 + 1)
if (not source.startswith(BOM_UTF8) and if (not source.startswith(BOM_UTF8) and
cookie_re.match(source[0:end1]) is None and cookie_re.match(source[0:end1]) is None and
cookie_re.match(source[end1 + 1:end2]) is None): cookie_re.match(source[end1 + 1:end2]) is None):
if hasattr(state, "_indecode"): if hasattr(state, "_indecode"):
# encodings imported us again, so don't rewrite. # encodings imported us again, so don't rewrite.
return None, None return None, None
@ -336,7 +332,7 @@ def _rewrite_test(config, fn):
return None, None return None, None
rewrite_asserts(tree, fn, config) rewrite_asserts(tree, fn, config)
try: try:
co = compile(tree, fn.strpath, "exec") co = compile(tree, fn.strpath, "exec", dont_inherit=True)
except SyntaxError: except SyntaxError:
# It's possible that this error is from some bug in the # It's possible that this error is from some bug in the
# assertion rewriting, but I don't know of a fast way to tell. # assertion rewriting, but I don't know of a fast way to tell.
@ -344,6 +340,7 @@ def _rewrite_test(config, fn):
return None, None return None, None
return stat, co return stat, co
def _make_rewritten_pyc(state, source_stat, pyc, co): def _make_rewritten_pyc(state, source_stat, pyc, co):
"""Try to dump rewritten code to *pyc*.""" """Try to dump rewritten code to *pyc*."""
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
@ -357,6 +354,7 @@ def _make_rewritten_pyc(state, source_stat, pyc, co):
if _write_pyc(state, co, source_stat, proc_pyc): if _write_pyc(state, co, source_stat, proc_pyc):
os.rename(proc_pyc, pyc) os.rename(proc_pyc, pyc)
def _read_pyc(source, pyc, trace=lambda x: None): def _read_pyc(source, pyc, trace=lambda x: None):
"""Possibly read a pytest pyc containing rewritten code. """Possibly read a pytest pyc containing rewritten code.
@ -414,7 +412,8 @@ def _saferepr(obj):
return repr.replace(t("\n"), t("\\n")) return repr.replace(t("\n"), t("\\n"))
from _pytest.assertion.util import format_explanation as _format_explanation # noqa from _pytest.assertion.util import format_explanation as _format_explanation # noqa
def _format_assertmsg(obj): def _format_assertmsg(obj):
"""Format the custom assertion message given. """Format the custom assertion message given.
@ -443,9 +442,11 @@ def _format_assertmsg(obj):
s = s.replace(t("\\n"), t("\n~")) s = s.replace(t("\\n"), t("\n~"))
return s return s
def _should_repr_global_name(obj): def _should_repr_global_name(obj):
return not hasattr(obj, "__name__") and not py.builtin.callable(obj) return not hasattr(obj, "__name__") and not py.builtin.callable(obj)
def _format_boolop(explanations, is_or): def _format_boolop(explanations, is_or):
explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
if py.builtin._istext(explanation): if py.builtin._istext(explanation):
@ -454,6 +455,7 @@ def _format_boolop(explanations, is_or):
t = py.builtin.bytes t = py.builtin.bytes
return explanation.replace(t('%'), t('%%')) return explanation.replace(t('%'), t('%%'))
def _call_reprcompare(ops, results, expls, each_obj): def _call_reprcompare(ops, results, expls, each_obj):
for i, res, expl in zip(range(len(ops)), results, expls): for i, res, expl in zip(range(len(ops)), results, expls):
try: try:
@ -487,7 +489,7 @@ def _call_reprcompare(ops, results, expls, each_obj):
ast.Mult: "*", ast.Mult: "*",
ast.Div: "/", ast.Div: "/",
ast.FloorDiv: "//", ast.FloorDiv: "//",
ast.Mod: "%%", # escaped for string formatting ast.Mod: "%%", # escaped for string formatting
ast.Eq: "==", ast.Eq: "==",
ast.NotEq: "!=", ast.NotEq: "!=",
ast.Lt: "<", ast.Lt: "<",
@ -593,23 +595,26 @@ def run(self, mod):
# docstrings and __future__ imports. # docstrings and __future__ imports.
aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"),
ast.alias("_pytest.assertion.rewrite", "@pytest_ar")] ast.alias("_pytest.assertion.rewrite", "@pytest_ar")]
expect_docstring = True doc = getattr(mod, "docstring", None)
expect_docstring = doc is None
if doc is not None and self.is_rewrite_disabled(doc):
return
pos = 0 pos = 0
lineno = 0 lineno = 1
for item in mod.body: for item in mod.body:
if (expect_docstring and isinstance(item, ast.Expr) and if (expect_docstring and isinstance(item, ast.Expr) and
isinstance(item.value, ast.Str)): isinstance(item.value, ast.Str)):
doc = item.value.s doc = item.value.s
if "PYTEST_DONT_REWRITE" in doc: if self.is_rewrite_disabled(doc):
# The module has disabled assertion rewriting.
return return
lineno += len(doc) - 1
expect_docstring = False expect_docstring = False
elif (not isinstance(item, ast.ImportFrom) or item.level > 0 or elif (not isinstance(item, ast.ImportFrom) or item.level > 0 or
item.module != "__future__"): item.module != "__future__"):
lineno = item.lineno lineno = item.lineno
break break
pos += 1 pos += 1
else:
lineno = item.lineno
imports = [ast.Import([alias], lineno=lineno, col_offset=0) imports = [ast.Import([alias], lineno=lineno, col_offset=0)
for alias in aliases] for alias in aliases]
mod.body[pos:pos] = imports mod.body[pos:pos] = imports
@ -635,6 +640,9 @@ def run(self, mod):
not isinstance(field, ast.expr)): not isinstance(field, ast.expr)):
nodes.append(field) nodes.append(field)
def is_rewrite_disabled(self, docstring):
return "PYTEST_DONT_REWRITE" in docstring
def variable(self): def variable(self):
"""Get a new variable.""" """Get a new variable."""
# Use a character invalid in python identifiers to avoid clashing. # Use a character invalid in python identifiers to avoid clashing.
@ -727,7 +735,7 @@ def visit_Assert(self, assert_):
if isinstance(assert_.test, ast.Tuple) and self.config is not None: if isinstance(assert_.test, ast.Tuple) and self.config is not None:
fslocation = (self.module_path, assert_.lineno) fslocation = (self.module_path, assert_.lineno)
self.config.warn('R1', 'assertion is always true, perhaps ' self.config.warn('R1', 'assertion is always true, perhaps '
'remove parentheses?', fslocation=fslocation) 'remove parentheses?', fslocation=fslocation)
self.statements = [] self.statements = []
self.variables = [] self.variables = []
self.variable_counter = itertools.count() self.variable_counter = itertools.count()
@ -791,7 +799,7 @@ def visit_BoolOp(self, boolop):
if i: if i:
fail_inner = [] fail_inner = []
# cond is set in a prior loop iteration below # cond is set in a prior loop iteration below
self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa
self.on_failure = fail_inner self.on_failure = fail_inner
self.push_format_context() self.push_format_context()
res, expl = self.visit(v) res, expl = self.visit(v)
@ -843,7 +851,7 @@ def visit_Call_35(self, call):
new_kwargs.append(ast.keyword(keyword.arg, res)) new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg: if keyword.arg:
arg_expls.append(keyword.arg + "=" + expl) arg_expls.append(keyword.arg + "=" + expl)
else: ## **args have `arg` keywords with an .arg of None else: # **args have `arg` keywords with an .arg of None
arg_expls.append("**" + expl) arg_expls.append("**" + expl)
expl = "%s(%s)" % (func_expl, ', '.join(arg_expls)) expl = "%s(%s)" % (func_expl, ', '.join(arg_expls))
@ -897,7 +905,6 @@ def visit_Call_legacy(self, call):
else: else:
visit_Call = visit_Call_legacy visit_Call = visit_Call_legacy
def visit_Attribute(self, attr): def visit_Attribute(self, attr):
if not isinstance(attr.ctx, ast.Load): if not isinstance(attr.ctx, ast.Load):
return self.generic_visit(attr) return self.generic_visit(attr)

View File

@ -0,0 +1,102 @@
"""
Utilities for truncating assertion output.
Current default behaviour is to truncate assertion explanations at
~8 terminal lines, unless running in "-vv" mode or running on CI.
"""
from __future__ import absolute_import, division, print_function
import os
import py
DEFAULT_MAX_LINES = 8
DEFAULT_MAX_CHARS = 8 * 80
USAGE_MSG = "use '-vv' to show"
def truncate_if_required(explanation, item, max_length=None):
"""
Truncate this assertion explanation if the given test item is eligible.
"""
if _should_truncate_item(item):
return _truncate_explanation(explanation)
return explanation
def _should_truncate_item(item):
"""
Whether or not this test item is eligible for truncation.
"""
verbose = item.config.option.verbose
return verbose < 2 and not _running_on_ci()
def _running_on_ci():
"""Check if we're currently running on a CI system."""
env_vars = ['CI', 'BUILD_NUMBER']
return any(var in os.environ for var in env_vars)
def _truncate_explanation(input_lines, max_lines=None, max_chars=None):
"""
Truncate given list of strings that makes up the assertion explanation.
Truncates to either 8 lines, or 640 characters - whichever the input reaches
first. The remaining lines will be replaced by a usage message.
"""
if max_lines is None:
max_lines = DEFAULT_MAX_LINES
if max_chars is None:
max_chars = DEFAULT_MAX_CHARS
# Check if truncation required
input_char_count = len("".join(input_lines))
if len(input_lines) <= max_lines and input_char_count <= max_chars:
return input_lines
# Truncate first to max_lines, and then truncate to max_chars if max_chars
# is exceeded.
truncated_explanation = input_lines[:max_lines]
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
# Add ellipsis to final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."
# Append useful message to explanation
truncated_line_count = len(input_lines) - len(truncated_explanation)
truncated_line_count += 1 # Account for the part-truncated final line
msg = '...Full output truncated'
if truncated_line_count == 1:
msg += ' ({0} line hidden)'.format(truncated_line_count)
else:
msg += ' ({0} lines hidden)'.format(truncated_line_count)
msg += ", {0}" .format(USAGE_MSG)
truncated_explanation.extend([
py.builtin._totext(""),
py.builtin._totext(msg),
])
return truncated_explanation
def _truncate_by_char_count(input_lines, max_chars):
# Check if truncation required
if len("".join(input_lines)) <= max_chars:
return input_lines
# Find point at which input length exceeds total allowed length
iterated_char_count = 0
for iterated_index, input_line in enumerate(input_lines):
if iterated_char_count + len(input_line) > max_chars:
break
iterated_char_count += len(input_line)
# Create truncated explanation with modified final line
truncated_result = input_lines[:iterated_index]
final_line = input_lines[iterated_index]
if final_line:
final_line_truncate_point = max_chars - iterated_char_count
final_line = final_line[:final_line_truncate_point]
truncated_result.append(final_line)
return truncated_result

View File

@ -1,4 +1,5 @@
"""Utilities for assertion debugging""" """Utilities for assertion debugging"""
from __future__ import absolute_import, division, print_function
import pprint import pprint
import _pytest._code import _pytest._code
@ -8,7 +9,7 @@
except ImportError: except ImportError:
Sequence = list Sequence = list
BuiltinAssertionError = py.builtin.builtins.AssertionError
u = py.builtin._totext u = py.builtin._totext
# The _reprcompare attribute on the util module is used by the new assertion # The _reprcompare attribute on the util module is used by the new assertion
@ -52,11 +53,11 @@ def _split_explanation(explanation):
""" """
raw_lines = (explanation or u('')).split('\n') raw_lines = (explanation or u('')).split('\n')
lines = [raw_lines[0]] lines = [raw_lines[0]]
for l in raw_lines[1:]: for values in raw_lines[1:]:
if l and l[0] in ['{', '}', '~', '>']: if values and values[0] in ['{', '}', '~', '>']:
lines.append(l) lines.append(values)
else: else:
lines[-1] += '\\n' + l lines[-1] += '\\n' + values
return lines return lines
@ -81,7 +82,7 @@ def _format_lines(lines):
stack.append(len(result)) stack.append(len(result))
stackcnt[-1] += 1 stackcnt[-1] += 1
stackcnt.append(0) stackcnt.append(0)
result.append(u(' +') + u(' ')*(len(stack)-1) + s + line[1:]) result.append(u(' +') + u(' ') * (len(stack) - 1) + s + line[1:])
elif line.startswith('}'): elif line.startswith('}'):
stack.pop() stack.pop()
stackcnt.pop() stackcnt.pop()
@ -90,7 +91,7 @@ def _format_lines(lines):
assert line[0] in ['~', '>'] assert line[0] in ['~', '>']
stack[-1] += 1 stack[-1] += 1
indent = len(stack) if line.startswith('~') else len(stack) - 1 indent = len(stack) if line.startswith('~') else len(stack) - 1
result.append(u(' ')*indent + line[1:]) result.append(u(' ') * indent + line[1:])
assert len(stack) == 1 assert len(stack) == 1
return result return result
@ -105,16 +106,22 @@ def _format_lines(lines):
def assertrepr_compare(config, op, left, right): def assertrepr_compare(config, op, left, right):
"""Return specialised explanations for some operators/operands""" """Return specialised explanations for some operators/operands"""
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
left_repr = py.io.saferepr(left, maxsize=int(width//2)) left_repr = py.io.saferepr(left, maxsize=int(width // 2))
right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) right_repr = py.io.saferepr(right, maxsize=width - len(left_repr))
summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr)) summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr))
issequence = lambda x: (isinstance(x, (list, tuple, Sequence)) and def issequence(x):
not isinstance(x, basestring)) return (isinstance(x, (list, tuple, Sequence)) and not isinstance(x, basestring))
istext = lambda x: isinstance(x, basestring)
isdict = lambda x: isinstance(x, dict) def istext(x):
isset = lambda x: isinstance(x, (set, frozenset)) return isinstance(x, basestring)
def isdict(x):
return isinstance(x, dict)
def isset(x):
return isinstance(x, (set, frozenset))
def isiterable(obj): def isiterable(obj):
try: try:
@ -256,8 +263,8 @@ def _compare_eq_dict(left, right, verbose=False):
explanation = [] explanation = []
common = set(left).intersection(set(right)) common = set(left).intersection(set(right))
same = dict((k, left[k]) for k in common if left[k] == right[k]) same = dict((k, left[k]) for k in common if left[k] == right[k])
if same and not verbose: if same and verbose < 2:
explanation += [u('Omitting %s identical items, use -v to show') % explanation += [u('Omitting %s identical items, use -vv to show') %
len(same)] len(same)]
elif same: elif same:
explanation += [u('Common items:')] explanation += [u('Common items:')]
@ -284,7 +291,7 @@ def _compare_eq_dict(left, right, verbose=False):
def _notin_text(term, text, verbose=False): def _notin_text(term, text, verbose=False):
index = text.find(term) index = text.find(term)
head = text[:index] head = text[:index]
tail = text[index+len(term):] tail = text[index + len(term):]
correct_text = head + tail correct_text = head + tail
diff = _diff_text(correct_text, text, verbose) diff = _diff_text(correct_text, text, verbose)
newdiff = [u('%s is contained here:') % py.io.saferepr(term, maxsize=42)] newdiff = [u('%s is contained here:') % py.io.saferepr(term, maxsize=42)]

71
lib/spack/external/_pytest/cacheprovider.py vendored Normal file → Executable file
View File

@ -1,20 +1,21 @@
""" """
merged implementation of the cache provider merged implementation of the cache provider
the name cache was not choosen to ensure pluggy automatically the name cache was not chosen to ensure pluggy automatically
ignores the external pytest-cache ignores the external pytest-cache
""" """
from __future__ import absolute_import, division, print_function
import py import py
import pytest import pytest
import json import json
import os
from os.path import sep as _sep, altsep as _altsep from os.path import sep as _sep, altsep as _altsep
class Cache(object): class Cache(object):
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self._cachedir = config.rootdir.join(".cache") self._cachedir = Cache.cache_dir_from_config(config)
self.trace = config.trace.root.get("cache") self.trace = config.trace.root.get("cache")
if config.getvalue("cacheclear"): if config.getvalue("cacheclear"):
self.trace("clearing cachedir") self.trace("clearing cachedir")
@ -22,6 +23,16 @@ def __init__(self, config):
self._cachedir.remove() self._cachedir.remove()
self._cachedir.mkdir() self._cachedir.mkdir()
@staticmethod
def cache_dir_from_config(config):
cache_dir = config.getini("cache_dir")
cache_dir = os.path.expanduser(cache_dir)
cache_dir = os.path.expandvars(cache_dir)
if os.path.isabs(cache_dir):
return py.path.local(cache_dir)
else:
return config.rootdir.join(cache_dir)
def makedir(self, name): def makedir(self, name):
""" return a directory path object with the given name. If the """ return a directory path object with the given name. If the
directory does not yet exist, it will be created. You can use it directory does not yet exist, it will be created. You can use it
@ -89,31 +100,31 @@ def set(self, key, value):
class LFPlugin: class LFPlugin:
""" Plugin which implements the --lf (run last-failing) option """ """ Plugin which implements the --lf (run last-failing) option """
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
active_keys = 'lf', 'failedfirst' active_keys = 'lf', 'failedfirst'
self.active = any(config.getvalue(key) for key in active_keys) self.active = any(config.getvalue(key) for key in active_keys)
if self.active: self.lastfailed = config.cache.get("cache/lastfailed", {})
self.lastfailed = config.cache.get("cache/lastfailed", {}) self._previously_failed_count = None
else:
self.lastfailed = {}
def pytest_report_header(self): def pytest_report_collectionfinish(self):
if self.active: if self.active:
if not self.lastfailed: if not self._previously_failed_count:
mode = "run all (no recorded failures)" mode = "run all (no recorded failures)"
else: else:
mode = "rerun last %d failures%s" % ( noun = 'failure' if self._previously_failed_count == 1 else 'failures'
len(self.lastfailed), suffix = " first" if self.config.getvalue("failedfirst") else ""
" first" if self.config.getvalue("failedfirst") else "") mode = "rerun previous {count} {noun}{suffix}".format(
count=self._previously_failed_count, suffix=suffix, noun=noun
)
return "run-last-failure: %s" % mode return "run-last-failure: %s" % mode
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
if report.failed and "xfail" not in report.keywords: if (report.when == 'call' and report.passed) or report.skipped:
self.lastfailed.pop(report.nodeid, None)
elif report.failed:
self.lastfailed[report.nodeid] = True self.lastfailed[report.nodeid] = True
elif not report.failed:
if report.when == "call":
self.lastfailed.pop(report.nodeid, None)
def pytest_collectreport(self, report): def pytest_collectreport(self, report):
passed = report.outcome in ('passed', 'skipped') passed = report.outcome in ('passed', 'skipped')
@ -135,22 +146,24 @@ def pytest_collection_modifyitems(self, session, config, items):
previously_failed.append(item) previously_failed.append(item)
else: else:
previously_passed.append(item) previously_passed.append(item)
if not previously_failed and previously_passed: self._previously_failed_count = len(previously_failed)
if not previously_failed:
# running a subset of all tests with recorded failures outside # running a subset of all tests with recorded failures outside
# of the set of tests currently executing # of the set of tests currently executing
pass return
elif self.config.getvalue("failedfirst"): if self.config.getvalue("lf"):
items[:] = previously_failed + previously_passed
else:
items[:] = previously_failed items[:] = previously_failed
config.hook.pytest_deselected(items=previously_passed) config.hook.pytest_deselected(items=previously_passed)
else:
items[:] = previously_failed + previously_passed
def pytest_sessionfinish(self, session): def pytest_sessionfinish(self, session):
config = self.config config = self.config
if config.getvalue("cacheshow") or hasattr(config, "slaveinput"): if config.getvalue("cacheshow") or hasattr(config, "slaveinput"):
return return
prev_failed = config.cache.get("cache/lastfailed", None) is not None
if (session.testscollected and prev_failed) or self.lastfailed: saved_lastfailed = config.cache.get("cache/lastfailed", {})
if saved_lastfailed != self.lastfailed:
config.cache.set("cache/lastfailed", self.lastfailed) config.cache.set("cache/lastfailed", self.lastfailed)
@ -171,6 +184,9 @@ def pytest_addoption(parser):
group.addoption( group.addoption(
'--cache-clear', action='store_true', dest="cacheclear", '--cache-clear', action='store_true', dest="cacheclear",
help="remove all cache contents at start of test run.") help="remove all cache contents at start of test run.")
parser.addini(
"cache_dir", default='.cache',
help="cache directory path.")
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
@ -179,7 +195,6 @@ def pytest_cmdline_main(config):
return wrap_session(config, cacheshow) return wrap_session(config, cacheshow)
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_configure(config): def pytest_configure(config):
config.cache = Cache(config) config.cache = Cache(config)
@ -219,12 +234,12 @@ def cacheshow(config, session):
basedir = config.cache._cachedir basedir = config.cache._cachedir
vdir = basedir.join("v") vdir = basedir.join("v")
tw.sep("-", "cache values") tw.sep("-", "cache values")
for valpath in vdir.visit(lambda x: x.isfile()): for valpath in sorted(vdir.visit(lambda x: x.isfile())):
key = valpath.relto(vdir).replace(valpath.sep, "/") key = valpath.relto(vdir).replace(valpath.sep, "/")
val = config.cache.get(key, dummy) val = config.cache.get(key, dummy)
if val is dummy: if val is dummy:
tw.line("%s contains unreadable content, " tw.line("%s contains unreadable content, "
"will be ignored" % key) "will be ignored" % key)
else: else:
tw.line("%s contains:" % key) tw.line("%s contains:" % key)
stream = py.io.TextIO() stream = py.io.TextIO()
@ -235,8 +250,8 @@ def cacheshow(config, session):
ddir = basedir.join("d") ddir = basedir.join("d")
if ddir.isdir() and ddir.listdir(): if ddir.isdir() and ddir.listdir():
tw.sep("-", "cache directories") tw.sep("-", "cache directories")
for p in basedir.join("d").visit(): for p in sorted(basedir.join("d").visit()):
#if p.check(dir=1): # if p.check(dir=1):
# print("%s/" % p.relto(basedir)) # print("%s/" % p.relto(basedir))
if p.isfile(): if p.isfile():
key = p.relto(basedir) key = p.relto(basedir)

View File

@ -2,17 +2,19 @@
per-test stdout/stderr capturing mechanism. per-test stdout/stderr capturing mechanism.
""" """
from __future__ import with_statement from __future__ import absolute_import, division, print_function
import contextlib import contextlib
import sys import sys
import os import os
import io
from io import UnsupportedOperation
from tempfile import TemporaryFile from tempfile import TemporaryFile
import py import py
import pytest import pytest
from _pytest.compat import CaptureIO
from py.io import TextIO
unicode = py.builtin.text unicode = py.builtin.text
patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'} patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
@ -32,8 +34,11 @@ def pytest_addoption(parser):
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_load_initial_conftests(early_config, parser, args): def pytest_load_initial_conftests(early_config, parser, args):
_readline_workaround()
ns = early_config.known_args_namespace ns = early_config.known_args_namespace
if ns.capture == "fd":
_py36_windowsconsoleio_workaround(sys.stdout)
_colorama_workaround()
_readline_workaround()
pluginmanager = early_config.pluginmanager pluginmanager = early_config.pluginmanager
capman = CaptureManager(ns.capture) capman = CaptureManager(ns.capture)
pluginmanager.register(capman, "capturemanager") pluginmanager.register(capman, "capturemanager")
@ -130,7 +135,7 @@ def pytest_runtest_call(self, item):
self.resumecapture() self.resumecapture()
self.activate_funcargs(item) self.activate_funcargs(item)
yield yield
#self.deactivate_funcargs() called from suspendcapture() # self.deactivate_funcargs() called from suspendcapture()
self.suspendcapture_item(item, "call") self.suspendcapture_item(item, "call")
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
@ -167,6 +172,7 @@ def capsys(request):
request.node._capfuncarg = c = CaptureFixture(SysCapture, request) request.node._capfuncarg = c = CaptureFixture(SysCapture, request)
return c return c
@pytest.fixture @pytest.fixture
def capfd(request): def capfd(request):
"""Enable capturing of writes to file descriptors 1 and 2 and make """Enable capturing of writes to file descriptors 1 and 2 and make
@ -234,6 +240,7 @@ def safe_text_dupfile(f, mode, default_encoding="UTF8"):
class EncodedFile(object): class EncodedFile(object):
errors = "strict" # possibly needed by py3 code (issue555) errors = "strict" # possibly needed by py3 code (issue555)
def __init__(self, buffer, encoding): def __init__(self, buffer, encoding):
self.buffer = buffer self.buffer = buffer
self.encoding = encoding self.encoding = encoding
@ -247,6 +254,11 @@ def writelines(self, linelist):
data = ''.join(linelist) data = ''.join(linelist)
self.write(data) self.write(data)
@property
def name(self):
"""Ensure that file.name is a string."""
return repr(self.buffer)
def __getattr__(self, name): def __getattr__(self, name):
return getattr(object.__getattribute__(self, "buffer"), name) return getattr(object.__getattribute__(self, "buffer"), name)
@ -314,9 +326,11 @@ def readouterr(self):
return (self.out.snap() if self.out is not None else "", return (self.out.snap() if self.out is not None else "",
self.err.snap() if self.err is not None else "") self.err.snap() if self.err is not None else "")
class NoCapture: class NoCapture:
__init__ = start = done = suspend = resume = lambda *args: None __init__ = start = done = suspend = resume = lambda *args: None
class FDCapture: class FDCapture:
""" Capture IO to/from a given os-level filedescriptor. """ """ Capture IO to/from a given os-level filedescriptor. """
@ -389,7 +403,7 @@ def resume(self):
def writeorg(self, data): def writeorg(self, data):
""" write to original file descriptor. """ """ write to original file descriptor. """
if py.builtin._istext(data): if py.builtin._istext(data):
data = data.encode("utf8") # XXX use encoding of original stream data = data.encode("utf8") # XXX use encoding of original stream
os.write(self.targetfd_save, data) os.write(self.targetfd_save, data)
@ -402,7 +416,7 @@ def __init__(self, fd, tmpfile=None):
if name == "stdin": if name == "stdin":
tmpfile = DontReadFromInput() tmpfile = DontReadFromInput()
else: else:
tmpfile = TextIO() tmpfile = CaptureIO()
self.tmpfile = tmpfile self.tmpfile = tmpfile
def start(self): def start(self):
@ -448,7 +462,8 @@ def read(self, *args):
__iter__ = read __iter__ = read
def fileno(self): def fileno(self):
raise ValueError("redirected Stdin is pseudofile, has no fileno()") raise UnsupportedOperation("redirected stdin is pseudofile, "
"has no fileno()")
def isatty(self): def isatty(self):
return False return False
@ -458,12 +473,30 @@ def close(self):
@property @property
def buffer(self): def buffer(self):
if sys.version_info >= (3,0): if sys.version_info >= (3, 0):
return self return self
else: else:
raise AttributeError('redirected stdin has no attribute buffer') raise AttributeError('redirected stdin has no attribute buffer')
def _colorama_workaround():
"""
Ensure colorama is imported so that it attaches to the correct stdio
handles on Windows.
colorama uses the terminal on import time. So if something does the
first import of colorama while I/O capture is active, colorama will
fail in various ways.
"""
if not sys.platform.startswith('win32'):
return
try:
import colorama # noqa
except ImportError:
pass
def _readline_workaround(): def _readline_workaround():
""" """
Ensure readline is imported so that it attaches to the correct stdio Ensure readline is imported so that it attaches to the correct stdio
@ -489,3 +522,56 @@ def _readline_workaround():
import readline # noqa import readline # noqa
except ImportError: except ImportError:
pass pass
def _py36_windowsconsoleio_workaround(stream):
"""
Python 3.6 implemented unicode console handling for Windows. This works
by reading/writing to the raw console handle using
``{Read,Write}ConsoleW``.
The problem is that we are going to ``dup2`` over the stdio file
descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
handles used by Python to write to the console. Though there is still some
weirdness and the console handle seems to only be closed randomly and not
on the first call to ``CloseHandle``, or maybe it gets reopened with the
same handle value when we suspend capturing.
The workaround in this case will reopen stdio with a different fd which
also means a different handle by replicating the logic in
"Py_lifecycle.c:initstdio/create_stdio".
:param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given
here as parameter for unittesting purposes.
See https://github.com/pytest-dev/py/issues/103
"""
if not sys.platform.startswith('win32') or sys.version_info[:2] < (3, 6):
return
# bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666)
if not hasattr(stream, 'buffer'):
return
buffered = hasattr(stream.buffer, 'raw')
raw_stdout = stream.buffer.raw if buffered else stream.buffer
if not isinstance(raw_stdout, io._WindowsConsoleIO):
return
def _reopen_stdio(f, mode):
if not buffered and mode[0] == 'w':
buffering = 0
else:
buffering = -1
return io.TextIOWrapper(
open(os.dup(f.fileno()), mode, buffering),
f.encoding,
f.errors,
f.newlines,
f.line_buffering)
sys.__stdin__ = sys.stdin = _reopen_stdio(sys.stdin, 'rb')
sys.__stdout__ = sys.stdout = _reopen_stdio(sys.stdout, 'wb')
sys.__stderr__ = sys.stderr = _reopen_stdio(sys.stderr, 'wb')

View File

@ -1,6 +1,7 @@
""" """
python version compatibility code python version compatibility code
""" """
from __future__ import absolute_import, division, print_function
import sys import sys
import inspect import inspect
import types import types
@ -9,8 +10,8 @@
import py import py
import _pytest import _pytest
from _pytest.outcomes import TEST_OUTCOME
try: try:
@ -19,6 +20,7 @@
# Only available in Python 3.4+ or as a backport # Only available in Python 3.4+ or as a backport
enum = None enum = None
_PY3 = sys.version_info > (3, 0) _PY3 = sys.version_info > (3, 0)
_PY2 = not _PY3 _PY2 = not _PY3
@ -26,6 +28,10 @@
NoneType = type(None) NoneType = type(None)
NOTSET = object() NOTSET = object()
PY35 = sys.version_info[:2] >= (3, 5)
PY36 = sys.version_info[:2] >= (3, 6)
MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError'
if hasattr(inspect, 'signature'): if hasattr(inspect, 'signature'):
def _format_args(func): def _format_args(func):
return str(inspect.signature(func)) return str(inspect.signature(func))
@ -42,11 +48,18 @@ def _format_args(func):
def is_generator(func): def is_generator(func):
try: genfunc = inspect.isgeneratorfunction(func)
return _pytest._code.getrawcode(func).co_flags & 32 # generator function return genfunc and not iscoroutinefunction(func)
except AttributeError: # builtin functions have no bytecode
# assume them to not be generators
return False def iscoroutinefunction(func):
"""Return True if func is a decorated coroutine function.
Note: copied and modified from Python 3.5's builtin couroutines.py to avoid import asyncio directly,
which in turns also initializes the "logging" module as side-effect (see issue #8).
"""
return (getattr(func, '_is_coroutine', False) or
(hasattr(inspect, 'iscoroutinefunction') and inspect.iscoroutinefunction(func)))
def getlocation(function, curdir): def getlocation(function, curdir):
@ -55,7 +68,7 @@ def getlocation(function, curdir):
lineno = py.builtin._getcode(function).co_firstlineno lineno = py.builtin._getcode(function).co_firstlineno
if fn.relto(curdir): if fn.relto(curdir):
fn = fn.relto(curdir) fn = fn.relto(curdir)
return "%s:%d" %(fn, lineno+1) return "%s:%d" % (fn, lineno + 1)
def num_mock_patch_args(function): def num_mock_patch_args(function):
@ -66,13 +79,21 @@ def num_mock_patch_args(function):
mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None)) mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None))
if mock is not None: if mock is not None:
return len([p for p in patchings return len([p for p in patchings
if not p.attribute_name and p.new is mock.DEFAULT]) if not p.attribute_name and p.new is mock.DEFAULT])
return len(patchings) return len(patchings)
def getfuncargnames(function, startindex=None): def getfuncargnames(function, startindex=None, cls=None):
"""
@RonnyPfannschmidt: This function should be refactored when we revisit fixtures. The
fixture mechanism should ask the node for the fixture names, and not try to obtain
directly from the function object well after collection has occurred.
"""
if startindex is None and cls is not None:
is_staticmethod = isinstance(cls.__dict__.get(function.__name__, None), staticmethod)
startindex = 0 if is_staticmethod else 1
# XXX merge with main.py's varnames # XXX merge with main.py's varnames
#assert not isclass(function) # assert not isclass(function)
realfunction = function realfunction = function
while hasattr(realfunction, "__wrapped__"): while hasattr(realfunction, "__wrapped__"):
realfunction = realfunction.__wrapped__ realfunction = realfunction.__wrapped__
@ -98,8 +119,7 @@ def getfuncargnames(function, startindex=None):
return tuple(argnames[startindex:]) return tuple(argnames[startindex:])
if sys.version_info[:2] == (2, 6):
if sys.version_info[:2] == (2, 6):
def isclass(object): def isclass(object):
""" Return true if the object is a class. Overrides inspect.isclass for """ Return true if the object is a class. Overrides inspect.isclass for
python 2.6 because it will return True for objects which always return python 2.6 because it will return True for objects which always return
@ -111,10 +131,12 @@ def isclass(object):
if _PY3: if _PY3:
import codecs import codecs
imap = map
izip = zip
STRING_TYPES = bytes, str STRING_TYPES = bytes, str
UNICODE_TYPES = str,
def _escape_strings(val): def _ascii_escaped(val):
"""If val is pure ascii, returns it as a str(). Otherwise, escapes """If val is pure ascii, returns it as a str(). Otherwise, escapes
bytes objects into a sequence of escaped bytes: bytes objects into a sequence of escaped bytes:
@ -144,8 +166,11 @@ def _escape_strings(val):
return val.encode('unicode_escape').decode('ascii') return val.encode('unicode_escape').decode('ascii')
else: else:
STRING_TYPES = bytes, str, unicode STRING_TYPES = bytes, str, unicode
UNICODE_TYPES = unicode,
def _escape_strings(val): from itertools import imap, izip # NOQA
def _ascii_escaped(val):
"""In py2 bytes and str are the same type, so return if it's a bytes """In py2 bytes and str are the same type, so return if it's a bytes
object, return it unchanged if it is a full ascii string, object, return it unchanged if it is a full ascii string,
otherwise escape it into its binary form. otherwise escape it into its binary form.
@ -167,8 +192,18 @@ def get_real_func(obj):
""" gets the real function object of the (possibly) wrapped object by """ gets the real function object of the (possibly) wrapped object by
functools.wraps or functools.partial. functools.wraps or functools.partial.
""" """
while hasattr(obj, "__wrapped__"): start_obj = obj
obj = obj.__wrapped__ for i in range(100):
new_obj = getattr(obj, '__wrapped__', None)
if new_obj is None:
break
obj = new_obj
else:
raise ValueError(
("could not find real function of {start}"
"\nstopped at {current}").format(
start=py.io.saferepr(start_obj),
current=py.io.saferepr(obj)))
if isinstance(obj, functools.partial): if isinstance(obj, functools.partial):
obj = obj.func obj = obj.func
return obj return obj
@ -195,14 +230,16 @@ def getimfunc(func):
def safe_getattr(object, name, default): def safe_getattr(object, name, default):
""" Like getattr but return default upon any Exception. """ Like getattr but return default upon any Exception or any OutcomeException.
Attribute access can potentially fail for 'evil' Python objects. Attribute access can potentially fail for 'evil' Python objects.
See issue214 See issue #214.
It catches OutcomeException because of #2490 (issue #580), new outcomes are derived from BaseException
instead of Exception (for more details check #2707)
""" """
try: try:
return getattr(object, name, default) return getattr(object, name, default)
except Exception: except TEST_OUTCOME:
return default return default
@ -226,5 +263,64 @@ def safe_str(v):
try: try:
return str(v) return str(v)
except UnicodeError: except UnicodeError:
if not isinstance(v, unicode):
v = unicode(v)
errors = 'replace' errors = 'replace'
return v.encode('ascii', errors) return v.encode('utf-8', errors)
COLLECT_FAKEMODULE_ATTRIBUTES = (
'Collector',
'Module',
'Generator',
'Function',
'Instance',
'Session',
'Item',
'Class',
'File',
'_fillfuncargs',
)
def _setup_collect_fakemodule():
from types import ModuleType
import pytest
pytest.collect = ModuleType('pytest.collect')
pytest.collect.__all__ = [] # used for setns
for attr in COLLECT_FAKEMODULE_ATTRIBUTES:
setattr(pytest.collect, attr, getattr(pytest, attr))
if _PY2:
# Without this the test_dupfile_on_textio will fail, otherwise CaptureIO could directly inherit from StringIO.
from py.io import TextIO
class CaptureIO(TextIO):
@property
def encoding(self):
return getattr(self, '_encoding', 'UTF-8')
else:
import io
class CaptureIO(io.TextIOWrapper):
def __init__(self):
super(CaptureIO, self).__init__(
io.BytesIO(),
encoding='UTF-8', newline='', write_through=True,
)
def getvalue(self):
return self.buffer.getvalue().decode('UTF-8')
class FuncargnamesCompatAttr(object):
""" helper class so that Metafunc, Function and FixtureRequest
don't need to each define the "funcargnames" compatibility attribute.
"""
@property
def funcargnames(self):
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
return self.fixturenames

View File

@ -1,4 +1,5 @@
""" command line options, ini-file and conftest.py processing. """ """ command line options, ini-file and conftest.py processing. """
from __future__ import absolute_import, division, print_function
import argparse import argparse
import shlex import shlex
import traceback import traceback
@ -7,7 +8,8 @@
import py import py
# DON't import pytest here because it causes import cycle troubles # DON't import pytest here because it causes import cycle troubles
import sys, os import sys
import os
import _pytest._code import _pytest._code
import _pytest.hookspec # the extension point definitions import _pytest.hookspec # the extension point definitions
import _pytest.assertion import _pytest.assertion
@ -53,15 +55,15 @@ def main(args=None, plugins=None):
return 4 return 4
else: else:
try: try:
config.pluginmanager.check_pending()
return config.hook.pytest_cmdline_main(config=config) return config.hook.pytest_cmdline_main(config=config)
finally: finally:
config._ensure_unconfigure() config._ensure_unconfigure()
except UsageError as e: except UsageError as e:
for msg in e.args: for msg in e.args:
sys.stderr.write("ERROR: %s\n" %(msg,)) sys.stderr.write("ERROR: %s\n" % (msg,))
return 4 return 4
class cmdline: # compatibility namespace class cmdline: # compatibility namespace
main = staticmethod(main) main = staticmethod(main)
@ -70,6 +72,12 @@ class UsageError(Exception):
""" error in pytest usage or invocation""" """ error in pytest usage or invocation"""
class PrintHelp(Exception):
"""Raised when pytest should print it's help to skip the rest of the
argument parsing and validation."""
pass
def filename_arg(path, optname): def filename_arg(path, optname):
""" Argparse type validator for filename arguments. """ Argparse type validator for filename arguments.
@ -95,10 +103,11 @@ def directory_arg(path, optname):
_preinit = [] _preinit = []
default_plugins = ( default_plugins = (
"mark main terminal runner python fixtures debugging unittest capture skipping " "mark main terminal runner python fixtures debugging unittest capture skipping "
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion "
"junitxml resultlog doctest cacheprovider freeze_support " "junitxml resultlog doctest cacheprovider freeze_support "
"setuponly setupplan").split() "setuponly setupplan warnings").split()
builtin_plugins = set(default_plugins) builtin_plugins = set(default_plugins)
builtin_plugins.add("pytester") builtin_plugins.add("pytester")
@ -108,6 +117,7 @@ def _preloadplugins():
assert not _preinit assert not _preinit
_preinit.append(get_config()) _preinit.append(get_config())
def get_config(): def get_config():
if _preinit: if _preinit:
return _preinit.pop(0) return _preinit.pop(0)
@ -118,6 +128,7 @@ def get_config():
pluginmanager.import_plugin(spec) pluginmanager.import_plugin(spec)
return config return config
def get_plugin_manager(): def get_plugin_manager():
""" """
Obtain a new instance of the Obtain a new instance of the
@ -129,6 +140,7 @@ def get_plugin_manager():
""" """
return get_config().pluginmanager return get_config().pluginmanager
def _prepareconfig(args=None, plugins=None): def _prepareconfig(args=None, plugins=None):
warning = None warning = None
if args is None: if args is None:
@ -153,7 +165,7 @@ def _prepareconfig(args=None, plugins=None):
if warning: if warning:
config.warn('C1', warning) config.warn('C1', warning)
return pluginmanager.hook.pytest_cmdline_parse( return pluginmanager.hook.pytest_cmdline_parse(
pluginmanager=pluginmanager, args=args) pluginmanager=pluginmanager, args=args)
except BaseException: except BaseException:
config._ensure_unconfigure() config._ensure_unconfigure()
raise raise
@ -161,13 +173,14 @@ def _prepareconfig(args=None, plugins=None):
class PytestPluginManager(PluginManager): class PytestPluginManager(PluginManager):
""" """
Overwrites :py:class:`pluggy.PluginManager` to add pytest-specific Overwrites :py:class:`pluggy.PluginManager <_pytest.vendored_packages.pluggy.PluginManager>` to add pytest-specific
functionality: functionality:
* loading plugins from the command line, ``PYTEST_PLUGIN`` env variable and * loading plugins from the command line, ``PYTEST_PLUGIN`` env variable and
``pytest_plugins`` global variables found in plugins being loaded; ``pytest_plugins`` global variables found in plugins being loaded;
* ``conftest.py`` loading during start-up; * ``conftest.py`` loading during start-up;
""" """
def __init__(self): def __init__(self):
super(PytestPluginManager, self).__init__("pytest", implprefix="pytest_") super(PytestPluginManager, self).__init__("pytest", implprefix="pytest_")
self._conftest_plugins = set() self._conftest_plugins = set()
@ -198,7 +211,8 @@ def addhooks(self, module_or_class):
""" """
.. deprecated:: 2.8 .. deprecated:: 2.8
Use :py:meth:`pluggy.PluginManager.add_hookspecs` instead. Use :py:meth:`pluggy.PluginManager.add_hookspecs <_pytest.vendored_packages.pluggy.PluginManager.add_hookspecs>`
instead.
""" """
warning = dict(code="I2", warning = dict(code="I2",
fslocation=_pytest._code.getfslineno(sys._getframe(1)), fslocation=_pytest._code.getfslineno(sys._getframe(1)),
@ -227,7 +241,7 @@ def parse_hookimpl_opts(self, plugin, name):
def parse_hookspec_opts(self, module_or_class, name): def parse_hookspec_opts(self, module_or_class, name):
opts = super(PytestPluginManager, self).parse_hookspec_opts( opts = super(PytestPluginManager, self).parse_hookspec_opts(
module_or_class, name) module_or_class, name)
if opts is None: if opts is None:
method = getattr(module_or_class, name) method = getattr(module_or_class, name)
if name.startswith("pytest_"): if name.startswith("pytest_"):
@ -250,7 +264,10 @@ def register(self, plugin, name=None):
ret = super(PytestPluginManager, self).register(plugin, name) ret = super(PytestPluginManager, self).register(plugin, name)
if ret: if ret:
self.hook.pytest_plugin_registered.call_historic( self.hook.pytest_plugin_registered.call_historic(
kwargs=dict(plugin=plugin, manager=self)) kwargs=dict(plugin=plugin, manager=self))
if isinstance(plugin, types.ModuleType):
self.consider_module(plugin)
return ret return ret
def getplugin(self, name): def getplugin(self, name):
@ -265,11 +282,11 @@ def pytest_configure(self, config):
# XXX now that the pluginmanager exposes hookimpl(tryfirst...) # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
# we should remove tryfirst/trylast as markers # we should remove tryfirst/trylast as markers
config.addinivalue_line("markers", config.addinivalue_line("markers",
"tryfirst: mark a hook implementation function such that the " "tryfirst: mark a hook implementation function such that the "
"plugin machinery will try to call it first/as early as possible.") "plugin machinery will try to call it first/as early as possible.")
config.addinivalue_line("markers", config.addinivalue_line("markers",
"trylast: mark a hook implementation function such that the " "trylast: mark a hook implementation function such that the "
"plugin machinery will try to call it last/as late as possible.") "plugin machinery will try to call it last/as late as possible.")
def _warn(self, message): def _warn(self, message):
kwargs = message if isinstance(message, dict) else { kwargs = message if isinstance(message, dict) else {
@ -293,7 +310,7 @@ def _set_initial_conftests(self, namespace):
""" """
current = py.path.local() current = py.path.local()
self._confcutdir = current.join(namespace.confcutdir, abs=True) \ self._confcutdir = current.join(namespace.confcutdir, abs=True) \
if namespace.confcutdir else None if namespace.confcutdir else None
self._noconftest = namespace.noconftest self._noconftest = namespace.noconftest
testpaths = namespace.file_or_dir testpaths = namespace.file_or_dir
foundanchor = False foundanchor = False
@ -304,7 +321,7 @@ def _set_initial_conftests(self, namespace):
if i != -1: if i != -1:
path = path[:i] path = path[:i]
anchor = current.join(path, abs=1) anchor = current.join(path, abs=1)
if exists(anchor): # we found some file object if exists(anchor): # we found some file object
self._try_load_conftest(anchor) self._try_load_conftest(anchor)
foundanchor = True foundanchor = True
if not foundanchor: if not foundanchor:
@ -371,7 +388,7 @@ def _importconftest(self, conftestpath):
if path and path.relto(dirpath) or path == dirpath: if path and path.relto(dirpath) or path == dirpath:
assert mod not in mods assert mod not in mods
mods.append(mod) mods.append(mod)
self.trace("loaded conftestmodule %r" %(mod)) self.trace("loaded conftestmodule %r" % (mod))
self.consider_conftest(mod) self.consider_conftest(mod)
return mod return mod
@ -381,7 +398,7 @@ def _importconftest(self, conftestpath):
# #
def consider_preparse(self, args): def consider_preparse(self, args):
for opt1,opt2 in zip(args, args[1:]): for opt1, opt2 in zip(args, args[1:]):
if opt1 == "-p": if opt1 == "-p":
self.consider_pluginarg(opt2) self.consider_pluginarg(opt2)
@ -395,38 +412,33 @@ def consider_pluginarg(self, arg):
self.import_plugin(arg) self.import_plugin(arg)
def consider_conftest(self, conftestmodule): def consider_conftest(self, conftestmodule):
if self.register(conftestmodule, name=conftestmodule.__file__): self.register(conftestmodule, name=conftestmodule.__file__)
self.consider_module(conftestmodule)
def consider_env(self): def consider_env(self):
self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
def consider_module(self, mod): def consider_module(self, mod):
plugins = getattr(mod, 'pytest_plugins', []) self._import_plugin_specs(getattr(mod, 'pytest_plugins', []))
if isinstance(plugins, str):
plugins = [plugins]
self.rewrite_hook.mark_rewrite(*plugins)
self._import_plugin_specs(plugins)
def _import_plugin_specs(self, spec): def _import_plugin_specs(self, spec):
if spec: plugins = _get_plugin_specs_as_list(spec)
if isinstance(spec, str): for import_spec in plugins:
spec = spec.split(",") self.import_plugin(import_spec)
for import_spec in spec:
self.import_plugin(import_spec)
def import_plugin(self, modname): def import_plugin(self, modname):
# most often modname refers to builtin modules, e.g. "pytester", # most often modname refers to builtin modules, e.g. "pytester",
# "terminal" or "capture". Those plugins are registered under their # "terminal" or "capture". Those plugins are registered under their
# basename for historic purposes but must be imported with the # basename for historic purposes but must be imported with the
# _pytest prefix. # _pytest prefix.
assert isinstance(modname, str) assert isinstance(modname, (py.builtin.text, str)), "module name as text required, got %r" % modname
modname = str(modname)
if self.get_plugin(modname) is not None: if self.get_plugin(modname) is not None:
return return
if modname in builtin_plugins: if modname in builtin_plugins:
importspec = "_pytest." + modname importspec = "_pytest." + modname
else: else:
importspec = modname importspec = modname
self.rewrite_hook.mark_rewrite(importspec)
try: try:
__import__(importspec) __import__(importspec)
except ImportError as e: except ImportError as e:
@ -440,11 +452,28 @@ def import_plugin(self, modname):
import pytest import pytest
if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception):
raise raise
self._warn("skipped plugin %r: %s" %((modname, e.msg))) self._warn("skipped plugin %r: %s" % ((modname, e.msg)))
else: else:
mod = sys.modules[importspec] mod = sys.modules[importspec]
self.register(mod, modname) self.register(mod, modname)
self.consider_module(mod)
def _get_plugin_specs_as_list(specs):
"""
Parses a list of "plugin specs" and returns a list of plugin names.
Plugin specs can be given as a list of strings separated by "," or already as a list/tuple in
which case it is returned as a list. Specs can also be `None` in which case an
empty list is returned.
"""
if specs is not None:
if isinstance(specs, str):
specs = specs.split(',') if specs else []
if not isinstance(specs, (list, tuple)):
raise UsageError("Plugin specs must be a ','-separated string or a "
"list/tuple of strings for plugin names. Given: %r" % specs)
return list(specs)
return []
class Parser: class Parser:
@ -488,7 +517,7 @@ def getgroup(self, name, description="", after=None):
for i, grp in enumerate(self._groups): for i, grp in enumerate(self._groups):
if grp.name == after: if grp.name == after:
break break
self._groups.insert(i+1, group) self._groups.insert(i + 1, group)
return group return group
def addoption(self, *opts, **attrs): def addoption(self, *opts, **attrs):
@ -526,7 +555,7 @@ def _getparser(self):
a = option.attrs() a = option.attrs()
arggroup.add_argument(*n, **a) arggroup.add_argument(*n, **a)
# bash like autocompletion for dirs (appending '/') # bash like autocompletion for dirs (appending '/')
optparser.add_argument(FILE_OR_DIR, nargs='*').completer=filescompleter optparser.add_argument(FILE_OR_DIR, nargs='*').completer = filescompleter
return optparser return optparser
def parse_setoption(self, args, option, namespace=None): def parse_setoption(self, args, option, namespace=None):
@ -670,7 +699,7 @@ def attrs(self):
if self._attrs.get('help'): if self._attrs.get('help'):
a = self._attrs['help'] a = self._attrs['help']
a = a.replace('%default', '%(default)s') a = a.replace('%default', '%(default)s')
#a = a.replace('%prog', '%(prog)s') # a = a.replace('%prog', '%(prog)s')
self._attrs['help'] = a self._attrs['help'] = a
return self._attrs return self._attrs
@ -754,7 +783,7 @@ def __init__(self, parser, extra_info=None):
extra_info = {} extra_info = {}
self._parser = parser self._parser = parser
argparse.ArgumentParser.__init__(self, usage=parser._usage, argparse.ArgumentParser.__init__(self, usage=parser._usage,
add_help=False, formatter_class=DropShorterLongHelpFormatter) add_help=False, formatter_class=DropShorterLongHelpFormatter)
# extra_info is a dict of (param -> value) to display if there's # extra_info is a dict of (param -> value) to display if there's
# an usage error to provide more contextual information to the user # an usage error to provide more contextual information to the user
self.extra_info = extra_info self.extra_info = extra_info
@ -782,9 +811,10 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
- shortcut if there are only two options and one of them is a short one - shortcut if there are only two options and one of them is a short one
- cache result on action object as this is called at least 2 times - cache result on action object as this is called at least 2 times
""" """
def _format_action_invocation(self, action): def _format_action_invocation(self, action):
orgstr = argparse.HelpFormatter._format_action_invocation(self, action) orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
if orgstr and orgstr[0] != '-': # only optional arguments if orgstr and orgstr[0] != '-': # only optional arguments
return orgstr return orgstr
res = getattr(action, '_formatted_action_invocation', None) res = getattr(action, '_formatted_action_invocation', None)
if res: if res:
@ -795,7 +825,7 @@ def _format_action_invocation(self, action):
action._formatted_action_invocation = orgstr action._formatted_action_invocation = orgstr
return orgstr return orgstr
return_list = [] return_list = []
option_map = getattr(action, 'map_long_option', {}) option_map = getattr(action, 'map_long_option', {})
if option_map is None: if option_map is None:
option_map = {} option_map = {}
short_long = {} short_long = {}
@ -813,7 +843,7 @@ def _format_action_invocation(self, action):
short_long[shortened] = xxoption short_long[shortened] = xxoption
# now short_long has been filled out to the longest with dashes # now short_long has been filled out to the longest with dashes
# **and** we keep the right option ordering from add_argument # **and** we keep the right option ordering from add_argument
for option in options: # for option in options:
if len(option) == 2 or option[2] == ' ': if len(option) == 2 or option[2] == ' ':
return_list.append(option) return_list.append(option)
if option[2:] == short_long.get(option.replace('-', '')): if option[2:] == short_long.get(option.replace('-', '')):
@ -822,22 +852,26 @@ def _format_action_invocation(self, action):
return action._formatted_action_invocation return action._formatted_action_invocation
def _ensure_removed_sysmodule(modname): def _ensure_removed_sysmodule(modname):
try: try:
del sys.modules[modname] del sys.modules[modname]
except KeyError: except KeyError:
pass pass
class CmdOptions(object): class CmdOptions(object):
""" holds cmdline options as attributes.""" """ holds cmdline options as attributes."""
def __init__(self, values=()): def __init__(self, values=()):
self.__dict__.update(values) self.__dict__.update(values)
def __repr__(self): def __repr__(self):
return "<CmdOptions %r>" %(self.__dict__,) return "<CmdOptions %r>" % (self.__dict__,)
def copy(self): def copy(self):
return CmdOptions(self.__dict__) return CmdOptions(self.__dict__)
class Notset: class Notset:
def __repr__(self): def __repr__(self):
return "<NOTSET>" return "<NOTSET>"
@ -847,6 +881,18 @@ def __repr__(self):
FILE_OR_DIR = 'file_or_dir' FILE_OR_DIR = 'file_or_dir'
def _iter_rewritable_modules(package_files):
for fn in package_files:
is_simple_module = '/' not in fn and fn.endswith('.py')
is_package = fn.count('/') == 1 and fn.endswith('__init__.py')
if is_simple_module:
module_name, _ = os.path.splitext(fn)
yield module_name
elif is_package:
package_name = os.path.dirname(fn)
yield package_name
class Config(object): class Config(object):
""" access to configuration values, pluginmanager and plugin hooks. """ """ access to configuration values, pluginmanager and plugin hooks. """
@ -864,6 +910,7 @@ def __init__(self, pluginmanager):
self.trace = self.pluginmanager.trace.root.get("config") self.trace = self.pluginmanager.trace.root.get("config")
self.hook = self.pluginmanager.hook self.hook = self.pluginmanager.hook
self._inicache = {} self._inicache = {}
self._override_ini = ()
self._opt2dest = {} self._opt2dest = {}
self._cleanup = [] self._cleanup = []
self._warn = self.pluginmanager._warn self._warn = self.pluginmanager._warn
@ -896,11 +943,11 @@ def _ensure_unconfigure(self):
fin = self._cleanup.pop() fin = self._cleanup.pop()
fin() fin()
def warn(self, code, message, fslocation=None): def warn(self, code, message, fslocation=None, nodeid=None):
""" generate a warning for this test session. """ """ generate a warning for this test session. """
self.hook.pytest_logwarning.call_historic(kwargs=dict( self.hook.pytest_logwarning.call_historic(kwargs=dict(
code=code, message=message, code=code, message=message,
fslocation=fslocation, nodeid=None)) fslocation=fslocation, nodeid=nodeid))
def get_terminal_writer(self): def get_terminal_writer(self):
return self.pluginmanager.get_plugin("terminalreporter")._tw return self.pluginmanager.get_plugin("terminalreporter")._tw
@ -916,14 +963,14 @@ def notify_exception(self, excinfo, option=None):
else: else:
style = "native" style = "native"
excrepr = excinfo.getrepr(funcargs=True, excrepr = excinfo.getrepr(funcargs=True,
showlocals=getattr(option, 'showlocals', False), showlocals=getattr(option, 'showlocals', False),
style=style, style=style,
) )
res = self.hook.pytest_internalerror(excrepr=excrepr, res = self.hook.pytest_internalerror(excrepr=excrepr,
excinfo=excinfo) excinfo=excinfo)
if not py.builtin.any(res): if not py.builtin.any(res):
for line in str(excrepr).split("\n"): for line in str(excrepr).split("\n"):
sys.stderr.write("INTERNALERROR> %s\n" %line) sys.stderr.write("INTERNALERROR> %s\n" % line)
sys.stderr.flush() sys.stderr.flush()
def cwd_relative_nodeid(self, nodeid): def cwd_relative_nodeid(self, nodeid):
@ -964,8 +1011,9 @@ def _initini(self, args):
self.invocation_dir = py.path.local() self.invocation_dir = py.path.local()
self._parser.addini('addopts', 'extra command line options', 'args') self._parser.addini('addopts', 'extra command line options', 'args')
self._parser.addini('minversion', 'minimally required pytest version') self._parser.addini('minversion', 'minimally required pytest version')
self._override_ini = ns.override_ini or ()
def _consider_importhook(self, args, entrypoint_name): def _consider_importhook(self, args):
"""Install the PEP 302 import hook if using assertion re-writing. """Install the PEP 302 import hook if using assertion re-writing.
Needs to parse the --assert=<mode> option from the commandline Needs to parse the --assert=<mode> option from the commandline
@ -980,26 +1028,34 @@ def _consider_importhook(self, args, entrypoint_name):
except SystemError: except SystemError:
mode = 'plain' mode = 'plain'
else: else:
import pkg_resources self._mark_plugins_for_rewrite(hook)
self.pluginmanager.rewrite_hook = hook
for entrypoint in pkg_resources.iter_entry_points('pytest11'):
# 'RECORD' available for plugins installed normally (pip install)
# 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
# for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
# so it shouldn't be an issue
for metadata in ('RECORD', 'SOURCES.txt'):
for entry in entrypoint.dist._get_metadata(metadata):
fn = entry.split(',')[0]
is_simple_module = os.sep not in fn and fn.endswith('.py')
is_package = fn.count(os.sep) == 1 and fn.endswith('__init__.py')
if is_simple_module:
module_name, ext = os.path.splitext(fn)
hook.mark_rewrite(module_name)
elif is_package:
package_name = os.path.dirname(fn)
hook.mark_rewrite(package_name)
self._warn_about_missing_assertion(mode) self._warn_about_missing_assertion(mode)
def _mark_plugins_for_rewrite(self, hook):
"""
Given an importhook, mark for rewrite any top-level
modules or packages in the distribution package for
all pytest plugins.
"""
import pkg_resources
self.pluginmanager.rewrite_hook = hook
# 'RECORD' available for plugins installed normally (pip install)
# 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
# for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
# so it shouldn't be an issue
metadata_files = 'RECORD', 'SOURCES.txt'
package_files = (
entry.split(',')[0]
for entrypoint in pkg_resources.iter_entry_points('pytest11')
for metadata in metadata_files
for entry in entrypoint.dist._get_metadata(metadata)
)
for name in _iter_rewritable_modules(package_files):
hook.mark_rewrite(name)
def _warn_about_missing_assertion(self, mode): def _warn_about_missing_assertion(self, mode):
try: try:
assert False assert False
@ -1023,19 +1079,17 @@ def _preparse(self, args, addopts=True):
args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args
args[:] = self.getini("addopts") + args args[:] = self.getini("addopts") + args
self._checkversion() self._checkversion()
entrypoint_name = 'pytest11' self._consider_importhook(args)
self._consider_importhook(args, entrypoint_name)
self.pluginmanager.consider_preparse(args) self.pluginmanager.consider_preparse(args)
self.pluginmanager.load_setuptools_entrypoints(entrypoint_name) self.pluginmanager.load_setuptools_entrypoints('pytest11')
self.pluginmanager.consider_env() self.pluginmanager.consider_env()
self.known_args_namespace = ns = self._parser.parse_known_args(args, namespace=self.option.copy()) self.known_args_namespace = ns = self._parser.parse_known_args(args, namespace=self.option.copy())
confcutdir = self.known_args_namespace.confcutdir
if self.known_args_namespace.confcutdir is None and self.inifile: if self.known_args_namespace.confcutdir is None and self.inifile:
confcutdir = py.path.local(self.inifile).dirname confcutdir = py.path.local(self.inifile).dirname
self.known_args_namespace.confcutdir = confcutdir self.known_args_namespace.confcutdir = confcutdir
try: try:
self.hook.pytest_load_initial_conftests(early_config=self, self.hook.pytest_load_initial_conftests(early_config=self,
args=args, parser=self._parser) args=args, parser=self._parser)
except ConftestImportFailure: except ConftestImportFailure:
e = sys.exc_info()[1] e = sys.exc_info()[1]
if ns.help or ns.version: if ns.help or ns.version:
@ -1053,28 +1107,32 @@ def _checkversion(self):
myver = pytest.__version__.split(".") myver = pytest.__version__.split(".")
if myver < ver: if myver < ver:
raise pytest.UsageError( raise pytest.UsageError(
"%s:%d: requires pytest-%s, actual pytest-%s'" %( "%s:%d: requires pytest-%s, actual pytest-%s'" % (
self.inicfg.config.path, self.inicfg.lineof('minversion'), self.inicfg.config.path, self.inicfg.lineof('minversion'),
minver, pytest.__version__)) minver, pytest.__version__))
def parse(self, args, addopts=True): def parse(self, args, addopts=True):
# parse given cmdline arguments into this config object. # parse given cmdline arguments into this config object.
assert not hasattr(self, 'args'), ( assert not hasattr(self, 'args'), (
"can only parse cmdline args at most once per Config object") "can only parse cmdline args at most once per Config object")
self._origargs = args self._origargs = args
self.hook.pytest_addhooks.call_historic( self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager)) kwargs=dict(pluginmanager=self.pluginmanager))
self._preparse(args, addopts=addopts) self._preparse(args, addopts=addopts)
# XXX deprecated hook: # XXX deprecated hook:
self.hook.pytest_cmdline_preparse(config=self, args=args) self.hook.pytest_cmdline_preparse(config=self, args=args)
args = self._parser.parse_setoption(args, self.option, namespace=self.option) self._parser.after_preparse = True
if not args: try:
cwd = os.getcwd() args = self._parser.parse_setoption(args, self.option, namespace=self.option)
if cwd == self.rootdir:
args = self.getini('testpaths')
if not args: if not args:
args = [cwd] cwd = os.getcwd()
self.args = args if cwd == self.rootdir:
args = self.getini('testpaths')
if not args:
args = [cwd]
self.args = args
except PrintHelp:
pass
def addinivalue_line(self, name, line): def addinivalue_line(self, name, line):
""" add a line to an ini-file option. The option must have been """ add a line to an ini-file option. The option must have been
@ -1082,12 +1140,12 @@ def addinivalue_line(self, name, line):
the first line in its value. """ the first line in its value. """
x = self.getini(name) x = self.getini(name)
assert isinstance(x, list) assert isinstance(x, list)
x.append(line) # modifies the cached list inline x.append(line) # modifies the cached list inline
def getini(self, name): def getini(self, name):
""" return configuration value from an :ref:`ini file <inifiles>`. If the """ return configuration value from an :ref:`ini file <inifiles>`. If the
specified name hasn't been registered through a prior specified name hasn't been registered through a prior
:py:func:`parser.addini <pytest.config.Parser.addini>` :py:func:`parser.addini <_pytest.config.Parser.addini>`
call (usually from a plugin), a ValueError is raised. """ call (usually from a plugin), a ValueError is raised. """
try: try:
return self._inicache[name] return self._inicache[name]
@ -1099,7 +1157,7 @@ def _getini(self, name):
try: try:
description, type, default = self._parser._inidict[name] description, type, default = self._parser._inidict[name]
except KeyError: except KeyError:
raise ValueError("unknown configuration value: %r" %(name,)) raise ValueError("unknown configuration value: %r" % (name,))
value = self._get_override_ini_value(name) value = self._get_override_ini_value(name)
if value is None: if value is None:
try: try:
@ -1112,10 +1170,10 @@ def _getini(self, name):
return [] return []
if type == "pathlist": if type == "pathlist":
dp = py.path.local(self.inicfg.config.path).dirpath() dp = py.path.local(self.inicfg.config.path).dirpath()
l = [] values = []
for relpath in shlex.split(value): for relpath in shlex.split(value):
l.append(dp.join(relpath, abs=True)) values.append(dp.join(relpath, abs=True))
return l return values
elif type == "args": elif type == "args":
return shlex.split(value) return shlex.split(value)
elif type == "linelist": elif type == "linelist":
@ -1132,13 +1190,13 @@ def _getconftest_pathlist(self, name, path):
except KeyError: except KeyError:
return None return None
modpath = py.path.local(mod.__file__).dirpath() modpath = py.path.local(mod.__file__).dirpath()
l = [] values = []
for relroot in relroots: for relroot in relroots:
if not isinstance(relroot, py.path.local): if not isinstance(relroot, py.path.local):
relroot = relroot.replace("/", py.path.local.sep) relroot = relroot.replace("/", py.path.local.sep)
relroot = modpath.join(relroot, abs=True) relroot = modpath.join(relroot, abs=True)
l.append(relroot) values.append(relroot)
return l return values
def _get_override_ini_value(self, name): def _get_override_ini_value(self, name):
value = None value = None
@ -1146,15 +1204,14 @@ def _get_override_ini_value(self, name):
# and -o foo1=bar1 -o foo2=bar2 options # and -o foo1=bar1 -o foo2=bar2 options
# always use the last item if multiple value set for same ini-name, # always use the last item if multiple value set for same ini-name,
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2 # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
if self.getoption("override_ini", None): for ini_config_list in self._override_ini:
for ini_config_list in self.option.override_ini: for ini_config in ini_config_list:
for ini_config in ini_config_list: try:
try: (key, user_ini_value) = ini_config.split("=", 1)
(key, user_ini_value) = ini_config.split("=", 1) except ValueError:
except ValueError: raise UsageError("-o/--override-ini expects option=value style.")
raise UsageError("-o/--override-ini expects option=value style.") if key == name:
if key == name: value = user_ini_value
value = user_ini_value
return value return value
def getoption(self, name, default=notset, skip=False): def getoption(self, name, default=notset, skip=False):
@ -1177,7 +1234,7 @@ def getoption(self, name, default=notset, skip=False):
return default return default
if skip: if skip:
import pytest import pytest
pytest.skip("no %r option found" %(name,)) pytest.skip("no %r option found" % (name,))
raise ValueError("no option named %r" % (name,)) raise ValueError("no option named %r" % (name,))
def getvalue(self, name, path=None): def getvalue(self, name, path=None):
@ -1188,12 +1245,14 @@ def getvalueorskip(self, name, path=None):
""" (deprecated, use getoption(skip=True)) """ """ (deprecated, use getoption(skip=True)) """
return self.getoption(name, skip=True) return self.getoption(name, skip=True)
def exists(path, ignore=EnvironmentError): def exists(path, ignore=EnvironmentError):
try: try:
return path.check() return path.check()
except ignore: except ignore:
return False return False
def getcfg(args, warnfunc=None): def getcfg(args, warnfunc=None):
""" """
Search the list of arguments for a valid ini-file for pytest, Search the list of arguments for a valid ini-file for pytest,
@ -1228,25 +1287,20 @@ def getcfg(args, warnfunc=None):
return None, None, None return None, None, None
def get_common_ancestor(args): def get_common_ancestor(paths):
# args are what we get after early command line parsing (usually
# strings, but can be py.path.local objects as well)
common_ancestor = None common_ancestor = None
for arg in args: for path in paths:
if str(arg)[0] == "-": if not path.exists():
continue
p = py.path.local(arg)
if not p.exists():
continue continue
if common_ancestor is None: if common_ancestor is None:
common_ancestor = p common_ancestor = path
else: else:
if p.relto(common_ancestor) or p == common_ancestor: if path.relto(common_ancestor) or path == common_ancestor:
continue continue
elif common_ancestor.relto(p): elif common_ancestor.relto(path):
common_ancestor = p common_ancestor = path
else: else:
shared = p.common(common_ancestor) shared = path.common(common_ancestor)
if shared is not None: if shared is not None:
common_ancestor = shared common_ancestor = shared
if common_ancestor is None: if common_ancestor is None:
@ -1257,9 +1311,29 @@ def get_common_ancestor(args):
def get_dirs_from_args(args): def get_dirs_from_args(args):
return [d for d in (py.path.local(x) for x in args def is_option(x):
if not str(x).startswith("-")) return str(x).startswith('-')
if d.exists()]
def get_file_part_from_node_id(x):
return str(x).split('::')[0]
def get_dir_from_path(path):
if path.isdir():
return path
return py.path.local(path.dirname)
# These look like paths but may not exist
possible_paths = (
py.path.local(get_file_part_from_node_id(arg))
for arg in args
if not is_option(arg)
)
return [
get_dir_from_path(path)
for path in possible_paths
if path.exists()
]
def determine_setup(inifile, args, warnfunc=None): def determine_setup(inifile, args, warnfunc=None):
@ -1282,7 +1356,7 @@ def determine_setup(inifile, args, warnfunc=None):
rootdir, inifile, inicfg = getcfg(dirs, warnfunc=warnfunc) rootdir, inifile, inicfg = getcfg(dirs, warnfunc=warnfunc)
if rootdir is None: if rootdir is None:
rootdir = get_common_ancestor([py.path.local(), ancestor]) rootdir = get_common_ancestor([py.path.local(), ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == os.sep is_fs_root = os.path.splitdrive(str(rootdir))[1] == '/'
if is_fs_root: if is_fs_root:
rootdir = ancestor rootdir = ancestor
return rootdir, inifile, inicfg or {} return rootdir, inifile, inicfg or {}
@ -1304,7 +1378,7 @@ def setns(obj, dic):
else: else:
setattr(obj, name, value) setattr(obj, name, value)
obj.__all__.append(name) obj.__all__.append(name)
#if obj != pytest: # if obj != pytest:
# pytest.__all__.append(name) # pytest.__all__.append(name)
setattr(pytest, name, value) setattr(pytest, name, value)

View File

@ -1,10 +1,8 @@
""" interactive debugging with PDB, the Python Debugger. """ """ interactive debugging with PDB, the Python Debugger. """
from __future__ import absolute_import from __future__ import absolute_import, division, print_function
import pdb import pdb
import sys import sys
import pytest
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("general") group = parser.getgroup("general")
@ -16,19 +14,17 @@ def pytest_addoption(parser):
help="start a custom interactive Python debugger on errors. " help="start a custom interactive Python debugger on errors. "
"For example: --pdbcls=IPython.terminal.debugger:TerminalPdb") "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb")
def pytest_namespace():
return {'set_trace': pytestPDB().set_trace}
def pytest_configure(config): def pytest_configure(config):
if config.getvalue("usepdb") or config.getvalue("usepdb_cls"): if config.getvalue("usepdb_cls"):
modname, classname = config.getvalue("usepdb_cls").split(":")
__import__(modname)
pdb_cls = getattr(sys.modules[modname], classname)
else:
pdb_cls = pdb.Pdb
if config.getvalue("usepdb"):
config.pluginmanager.register(PdbInvoke(), 'pdbinvoke') config.pluginmanager.register(PdbInvoke(), 'pdbinvoke')
if config.getvalue("usepdb_cls"):
modname, classname = config.getvalue("usepdb_cls").split(":")
__import__(modname)
pdb_cls = getattr(sys.modules[modname], classname)
else:
pdb_cls = pdb.Pdb
pytestPDB._pdb_cls = pdb_cls
old = (pdb.set_trace, pytestPDB._pluginmanager) old = (pdb.set_trace, pytestPDB._pluginmanager)
@ -37,30 +33,33 @@ def fin():
pytestPDB._config = None pytestPDB._config = None
pytestPDB._pdb_cls = pdb.Pdb pytestPDB._pdb_cls = pdb.Pdb
pdb.set_trace = pytest.set_trace pdb.set_trace = pytestPDB.set_trace
pytestPDB._pluginmanager = config.pluginmanager pytestPDB._pluginmanager = config.pluginmanager
pytestPDB._config = config pytestPDB._config = config
pytestPDB._pdb_cls = pdb_cls
config._cleanup.append(fin) config._cleanup.append(fin)
class pytestPDB: class pytestPDB:
""" Pseudo PDB that defers to the real pdb. """ """ Pseudo PDB that defers to the real pdb. """
_pluginmanager = None _pluginmanager = None
_config = None _config = None
_pdb_cls = pdb.Pdb _pdb_cls = pdb.Pdb
def set_trace(self): @classmethod
def set_trace(cls):
""" invoke PDB set_trace debugging, dropping any IO capturing. """ """ invoke PDB set_trace debugging, dropping any IO capturing. """
import _pytest.config import _pytest.config
frame = sys._getframe().f_back frame = sys._getframe().f_back
if self._pluginmanager is not None: if cls._pluginmanager is not None:
capman = self._pluginmanager.getplugin("capturemanager") capman = cls._pluginmanager.getplugin("capturemanager")
if capman: if capman:
capman.suspendcapture(in_=True) capman.suspendcapture(in_=True)
tw = _pytest.config.create_terminal_writer(self._config) tw = _pytest.config.create_terminal_writer(cls._config)
tw.line() tw.line()
tw.sep(">", "PDB set_trace (IO-capturing turned off)") tw.sep(">", "PDB set_trace (IO-capturing turned off)")
self._pluginmanager.hook.pytest_enter_pdb(config=self._config) cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config)
self._pdb_cls().set_trace(frame) cls._pdb_cls().set_trace(frame)
class PdbInvoke: class PdbInvoke:
@ -74,7 +73,7 @@ def pytest_exception_interact(self, node, call, report):
def pytest_internalerror(self, excrepr, excinfo): def pytest_internalerror(self, excrepr, excinfo):
for line in str(excrepr).split("\n"): for line in str(excrepr).split("\n"):
sys.stderr.write("INTERNALERROR> %s\n" %line) sys.stderr.write("INTERNALERROR> %s\n" % line)
sys.stderr.flush() sys.stderr.flush()
tb = _postmortem_traceback(excinfo) tb = _postmortem_traceback(excinfo)
post_mortem(tb) post_mortem(tb)

View File

@ -5,10 +5,15 @@
Keeping it in a central location makes it easy to track what is deprecated and should Keeping it in a central location makes it easy to track what is deprecated and should
be removed when the time comes. be removed when the time comes.
""" """
from __future__ import absolute_import, division, print_function
class RemovedInPytest4Warning(DeprecationWarning):
"""warning class for features removed in pytest 4.0"""
MAIN_STR_ARGS = 'passing a string to pytest.main() is deprecated, ' \ MAIN_STR_ARGS = 'passing a string to pytest.main() is deprecated, ' \
'pass a list of arguments instead.' 'pass a list of arguments instead.'
YIELD_TESTS = 'yield tests are deprecated, and scheduled to be removed in pytest 4.0' YIELD_TESTS = 'yield tests are deprecated, and scheduled to be removed in pytest 4.0'
@ -21,4 +26,17 @@
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue" GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue"
RESULT_LOG = '--result-log is deprecated and scheduled for removal in pytest 4.0' RESULT_LOG = (
'--result-log is deprecated and scheduled for removal in pytest 4.0.\n'
'See https://docs.pytest.org/en/latest/usage.html#creating-resultlog-format-files for more information.'
)
MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
"MarkInfo objects are deprecated as they contain the merged marks"
)
MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
"Applying marks directly to parameters is deprecated,"
" please use pytest.param(..., marks=...) instead.\n"
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html"
)

View File

@ -1,5 +1,5 @@
""" discover and run doctests in modules and test files.""" """ discover and run doctests in modules and test files."""
from __future__ import absolute_import from __future__ import absolute_import, division, print_function
import traceback import traceback
@ -22,27 +22,29 @@
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE, DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
) )
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addini('doctest_optionflags', 'option flags for doctests', parser.addini('doctest_optionflags', 'option flags for doctests',
type="args", default=["ELLIPSIS"]) type="args", default=["ELLIPSIS"])
parser.addini("doctest_encoding", 'encoding used for doctest files', default="utf-8")
group = parser.getgroup("collect") group = parser.getgroup("collect")
group.addoption("--doctest-modules", group.addoption("--doctest-modules",
action="store_true", default=False, action="store_true", default=False,
help="run doctests in all .py modules", help="run doctests in all .py modules",
dest="doctestmodules") dest="doctestmodules")
group.addoption("--doctest-report", group.addoption("--doctest-report",
type=str.lower, default="udiff", type=str.lower, default="udiff",
help="choose another output format for diffs on doctest failure", help="choose another output format for diffs on doctest failure",
choices=DOCTEST_REPORT_CHOICES, choices=DOCTEST_REPORT_CHOICES,
dest="doctestreport") dest="doctestreport")
group.addoption("--doctest-glob", group.addoption("--doctest-glob",
action="append", default=[], metavar="pat", action="append", default=[], metavar="pat",
help="doctests file matching pattern, default: test*.txt", help="doctests file matching pattern, default: test*.txt",
dest="doctestglob") dest="doctestglob")
group.addoption("--doctest-ignore-import-errors", group.addoption("--doctest-ignore-import-errors",
action="store_true", default=False, action="store_true", default=False,
help="ignore doctest ImportErrors", help="ignore doctest ImportErrors",
dest="doctest_ignore_import_errors") dest="doctest_ignore_import_errors")
def pytest_collect_file(path, parent): def pytest_collect_file(path, parent):
@ -118,7 +120,7 @@ def repr_failure(self, excinfo):
lines = ["%03d %s" % (i + test.lineno + 1, x) lines = ["%03d %s" % (i + test.lineno + 1, x)
for (i, x) in enumerate(lines)] for (i, x) in enumerate(lines)]
# trim docstring error lines to 10 # trim docstring error lines to 10
lines = lines[example.lineno - 9:example.lineno + 1] lines = lines[max(example.lineno - 9, 0):example.lineno + 1]
else: else:
lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example'] lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example']
indent = '>>>' indent = '>>>'
@ -127,18 +129,18 @@ def repr_failure(self, excinfo):
indent = '...' indent = '...'
if excinfo.errisinstance(doctest.DocTestFailure): if excinfo.errisinstance(doctest.DocTestFailure):
lines += checker.output_difference(example, lines += checker.output_difference(example,
doctestfailure.got, report_choice).split("\n") doctestfailure.got, report_choice).split("\n")
else: else:
inner_excinfo = ExceptionInfo(excinfo.value.exc_info) inner_excinfo = ExceptionInfo(excinfo.value.exc_info)
lines += ["UNEXPECTED EXCEPTION: %s" % lines += ["UNEXPECTED EXCEPTION: %s" %
repr(inner_excinfo.value)] repr(inner_excinfo.value)]
lines += traceback.format_exception(*excinfo.value.exc_info) lines += traceback.format_exception(*excinfo.value.exc_info)
return ReprFailDoctest(reprlocation, lines) return ReprFailDoctest(reprlocation, lines)
else: else:
return super(DoctestItem, self).repr_failure(excinfo) return super(DoctestItem, self).repr_failure(excinfo)
def reportinfo(self): def reportinfo(self):
return self.fspath, None, "[doctest] %s" % self.name return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
def _get_flag_lookup(): def _get_flag_lookup():
@ -171,15 +173,16 @@ def collect(self):
# inspired by doctest.testfile; ideally we would use it directly, # inspired by doctest.testfile; ideally we would use it directly,
# but it doesn't support passing a custom checker # but it doesn't support passing a custom checker
text = self.fspath.read() encoding = self.config.getini("doctest_encoding")
text = self.fspath.read_text(encoding)
filename = str(self.fspath) filename = str(self.fspath)
name = self.fspath.basename name = self.fspath.basename
globs = {'__name__': '__main__'} globs = {'__name__': '__main__'}
optionflags = get_optionflags(self) optionflags = get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
checker=_get_checker()) checker=_get_checker())
_fix_spoof_python2(runner, encoding)
parser = doctest.DocTestParser() parser = doctest.DocTestParser()
test = parser.get_doctest(text, globs, name, filename, 0) test = parser.get_doctest(text, globs, name, filename, 0)
@ -215,6 +218,7 @@ def collect(self):
optionflags = get_optionflags(self) optionflags = get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
checker=_get_checker()) checker=_get_checker())
for test in finder.find(module, module.__name__): for test in finder.find(module, module.__name__):
if test.examples: # skip empty doctests if test.examples: # skip empty doctests
yield DoctestItem(test.name, self, runner, test) yield DoctestItem(test.name, self, runner, test)
@ -323,6 +327,33 @@ def _get_report_choice(key):
DOCTEST_REPORT_CHOICE_NONE: 0, DOCTEST_REPORT_CHOICE_NONE: 0,
}[key] }[key]
def _fix_spoof_python2(runner, encoding):
"""
Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. This
should patch only doctests for text files because they don't have a way to declare their
encoding. Doctests in docstrings from Python modules don't have the same problem given that
Python already decoded the strings.
This fixes the problem related in issue #2434.
"""
from _pytest.compat import _PY2
if not _PY2:
return
from doctest import _SpoofOut
class UnicodeSpoof(_SpoofOut):
def getvalue(self):
result = _SpoofOut.getvalue(self)
if encoding:
result = result.decode(encoding)
return result
runner._fakeout = UnicodeSpoof()
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def doctest_namespace(): def doctest_namespace():
""" """

View File

@ -1,22 +1,39 @@
import sys from __future__ import absolute_import, division, print_function
from py._code.code import FormattedExcinfo
import py
import pytest
import warnings
import inspect import inspect
import sys
import warnings
import py
from py._code.code import FormattedExcinfo
import _pytest import _pytest
from _pytest import nodes
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest.compat import ( from _pytest.compat import (
NOTSET, exc_clear, _format_args, NOTSET, exc_clear, _format_args,
getfslineno, get_real_func, getfslineno, get_real_func,
is_generator, isclass, getimfunc, is_generator, isclass, getimfunc,
getlocation, getfuncargnames, getlocation, getfuncargnames,
safe_getattr,
FuncargnamesCompatAttr,
) )
from _pytest.outcomes import fail, TEST_OUTCOME
if sys.version_info[:2] == (2, 6):
from ordereddict import OrderedDict
else:
from collections import OrderedDict # nopyqver
def pytest_sessionstart(session): def pytest_sessionstart(session):
import _pytest.python
scopename2class.update({
'class': _pytest.python.Class,
'module': _pytest.python.Module,
'function': _pytest.main.Item,
})
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
@ -29,6 +46,7 @@ def pytest_sessionstart(session):
scope2props["instance"] = scope2props["class"] + ("instance", ) scope2props["instance"] = scope2props["class"] + ("instance", )
scope2props["function"] = scope2props["instance"] + ("function", "keywords") scope2props["function"] = scope2props["instance"] + ("function", "keywords")
def scopeproperty(name=None, doc=None): def scopeproperty(name=None, doc=None):
def decoratescope(func): def decoratescope(func):
scopename = name or func.__name__ scopename = name or func.__name__
@ -43,19 +61,6 @@ def provide(self):
return decoratescope return decoratescope
def pytest_namespace():
scopename2class.update({
'class': pytest.Class,
'module': pytest.Module,
'function': pytest.Item,
})
return {
'fixture': fixture,
'yield_fixture': yield_fixture,
'collect': {'_fillfuncargs': fillfixtures}
}
def get_scope_node(node, scope): def get_scope_node(node, scope):
cls = scopename2class.get(scope) cls = scopename2class.get(scope)
if cls is None: if cls is None:
@ -73,7 +78,7 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager):
# XXX we can probably avoid this algorithm if we modify CallSpec2 # XXX we can probably avoid this algorithm if we modify CallSpec2
# to directly care for creating the fixturedefs within its methods. # to directly care for creating the fixturedefs within its methods.
if not metafunc._calls[0].funcargs: if not metafunc._calls[0].funcargs:
return # this function call does not have direct parametrization return # this function call does not have direct parametrization
# collect funcargs of all callspecs into a list of values # collect funcargs of all callspecs into a list of values
arg2params = {} arg2params = {}
arg2scope = {} arg2scope = {}
@ -103,36 +108,32 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager):
if scope != "function": if scope != "function":
node = get_scope_node(collector, scope) node = get_scope_node(collector, scope)
if node is None: if node is None:
assert scope == "class" and isinstance(collector, pytest.Module) assert scope == "class" and isinstance(collector, _pytest.python.Module)
# use module-level collector for class-scope (for now) # use module-level collector for class-scope (for now)
node = collector node = collector
if node and argname in node._name2pseudofixturedef: if node and argname in node._name2pseudofixturedef:
arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]] arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]]
else: else:
fixturedef = FixtureDef(fixturemanager, '', argname, fixturedef = FixtureDef(fixturemanager, '', argname,
get_direct_param_fixture_func, get_direct_param_fixture_func,
arg2scope[argname], arg2scope[argname],
valuelist, False, False) valuelist, False, False)
arg2fixturedefs[argname] = [fixturedef] arg2fixturedefs[argname] = [fixturedef]
if node is not None: if node is not None:
node._name2pseudofixturedef[argname] = fixturedef node._name2pseudofixturedef[argname] = fixturedef
def getfixturemarker(obj): def getfixturemarker(obj):
""" return fixturemarker or None if it doesn't exist or raised """ return fixturemarker or None if it doesn't exist or raised
exceptions.""" exceptions."""
try: try:
return getattr(obj, "_pytestfixturefunction", None) return getattr(obj, "_pytestfixturefunction", None)
except KeyboardInterrupt: except TEST_OUTCOME:
raise
except Exception:
# some objects raise errors like request (from flask import request) # some objects raise errors like request (from flask import request)
# we don't expect them to be fixture functions # we don't expect them to be fixture functions
return None return None
def get_parametrized_fixture_keys(item, scopenum): def get_parametrized_fixture_keys(item, scopenum):
""" return list of keys for all parametrized arguments which match """ return list of keys for all parametrized arguments which match
the specified scope. """ the specified scope. """
@ -142,10 +143,10 @@ def get_parametrized_fixture_keys(item, scopenum):
except AttributeError: except AttributeError:
pass pass
else: else:
# cs.indictes.items() is random order of argnames but # cs.indices.items() is random order of argnames. Need to
# then again different functions (items) can change order of # sort this so that different calls to
# arguments so it doesn't matter much probably # get_parametrized_fixture_keys will be deterministic.
for argname, param_index in cs.indices.items(): for argname, param_index in sorted(cs.indices.items()):
if cs._arg2scopenum[argname] != scopenum: if cs._arg2scopenum[argname] != scopenum:
continue continue
if scopenum == 0: # session if scopenum == 0: # session
@ -167,20 +168,21 @@ def reorder_items(items):
for scopenum in range(0, scopenum_function): for scopenum in range(0, scopenum_function):
argkeys_cache[scopenum] = d = {} argkeys_cache[scopenum] = d = {}
for item in items: for item in items:
keys = set(get_parametrized_fixture_keys(item, scopenum)) keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum))
if keys: if keys:
d[item] = keys d[item] = keys
return reorder_items_atscope(items, set(), argkeys_cache, 0) return reorder_items_atscope(items, set(), argkeys_cache, 0)
def reorder_items_atscope(items, ignore, argkeys_cache, scopenum): def reorder_items_atscope(items, ignore, argkeys_cache, scopenum):
if scopenum >= scopenum_function or len(items) < 3: if scopenum >= scopenum_function or len(items) < 3:
return items return items
items_done = [] items_done = []
while 1: while 1:
items_before, items_same, items_other, newignore = \ items_before, items_same, items_other, newignore = \
slice_items(items, ignore, argkeys_cache[scopenum]) slice_items(items, ignore, argkeys_cache[scopenum])
items_before = reorder_items_atscope( items_before = reorder_items_atscope(
items_before, ignore, argkeys_cache,scopenum+1) items_before, ignore, argkeys_cache, scopenum + 1)
if items_same is None: if items_same is None:
# nothing to reorder in this scope # nothing to reorder in this scope
assert items_other is None assert items_other is None
@ -201,9 +203,9 @@ def slice_items(items, ignore, scoped_argkeys_cache):
for i, item in enumerate(it): for i, item in enumerate(it):
argkeys = scoped_argkeys_cache.get(item) argkeys = scoped_argkeys_cache.get(item)
if argkeys is not None: if argkeys is not None:
argkeys = argkeys.difference(ignore) newargkeys = OrderedDict.fromkeys(k for k in argkeys if k not in ignore)
if argkeys: # found a slicing key if newargkeys: # found a slicing key
slicing_argkey = argkeys.pop() slicing_argkey, _ = newargkeys.popitem()
items_before = items[:i] items_before = items[:i]
items_same = [item] items_same = [item]
items_other = [] items_other = []
@ -211,7 +213,7 @@ def slice_items(items, ignore, scoped_argkeys_cache):
for item in it: for item in it:
argkeys = scoped_argkeys_cache.get(item) argkeys = scoped_argkeys_cache.get(item)
if argkeys and slicing_argkey in argkeys and \ if argkeys and slicing_argkey in argkeys and \
slicing_argkey not in ignore: slicing_argkey not in ignore:
items_same.append(item) items_same.append(item)
else: else:
items_other.append(item) items_other.append(item)
@ -221,17 +223,6 @@ def slice_items(items, ignore, scoped_argkeys_cache):
return items, None, None, None return items, None, None, None
class FuncargnamesCompatAttr:
""" helper class so that Metafunc, Function and FixtureRequest
don't need to each define the "funcargnames" compatibility attribute.
"""
@property
def funcargnames(self):
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
return self.fixturenames
def fillfixtures(function): def fillfixtures(function):
""" fill missing funcargs for a test function. """ """ fill missing funcargs for a test function. """
try: try:
@ -254,10 +245,10 @@ def fillfixtures(function):
request._fillfixtures() request._fillfixtures()
def get_direct_param_fixture_func(request): def get_direct_param_fixture_func(request):
return request.param return request.param
class FuncFixtureInfo: class FuncFixtureInfo:
def __init__(self, argnames, names_closure, name2fixturedefs): def __init__(self, argnames, names_closure, name2fixturedefs):
self.argnames = argnames self.argnames = argnames
@ -296,7 +287,6 @@ def node(self):
""" underlying collection node (depends on current request scope)""" """ underlying collection node (depends on current request scope)"""
return self._getscopeitem(self.scope) return self._getscopeitem(self.scope)
def _getnextfixturedef(self, argname): def _getnextfixturedef(self, argname):
fixturedefs = self._arg2fixturedefs.get(argname, None) fixturedefs = self._arg2fixturedefs.get(argname, None)
if fixturedefs is None: if fixturedefs is None:
@ -318,7 +308,6 @@ def config(self):
""" the pytest config object associated with this request. """ """ the pytest config object associated with this request. """
return self._pyfuncitem.config return self._pyfuncitem.config
@scopeproperty() @scopeproperty()
def function(self): def function(self):
""" test function object if the request has a per-function scope. """ """ test function object if the request has a per-function scope. """
@ -327,7 +316,7 @@ def function(self):
@scopeproperty("class") @scopeproperty("class")
def cls(self): def cls(self):
""" class (can be None) where the test function was collected. """ """ class (can be None) where the test function was collected. """
clscol = self._pyfuncitem.getparent(pytest.Class) clscol = self._pyfuncitem.getparent(_pytest.python.Class)
if clscol: if clscol:
return clscol.obj return clscol.obj
@ -345,7 +334,7 @@ def instance(self):
@scopeproperty() @scopeproperty()
def module(self): def module(self):
""" python module object where the test function was collected. """ """ python module object where the test function was collected. """
return self._pyfuncitem.getparent(pytest.Module).obj return self._pyfuncitem.getparent(_pytest.python.Module).obj
@scopeproperty() @scopeproperty()
def fspath(self): def fspath(self):
@ -414,7 +403,7 @@ def cached_setup(self, setup, teardown=None, scope="module", extrakey=None):
:arg extrakey: added to internal caching key of (funcargname, scope). :arg extrakey: added to internal caching key of (funcargname, scope).
""" """
if not hasattr(self.config, '_setupcache'): if not hasattr(self.config, '_setupcache'):
self.config._setupcache = {} # XXX weakref? self.config._setupcache = {} # XXX weakref?
cachekey = (self.fixturename, self._getscopeitem(scope), extrakey) cachekey = (self.fixturename, self._getscopeitem(scope), extrakey)
cache = self.config._setupcache cache = self.config._setupcache
try: try:
@ -445,7 +434,8 @@ def getfuncargvalue(self, argname):
from _pytest import deprecated from _pytest import deprecated
warnings.warn( warnings.warn(
deprecated.GETFUNCARGVALUE, deprecated.GETFUNCARGVALUE,
DeprecationWarning) DeprecationWarning,
stacklevel=2)
return self.getfixturevalue(argname) return self.getfixturevalue(argname)
def _get_active_fixturedef(self, argname): def _get_active_fixturedef(self, argname):
@ -470,13 +460,13 @@ class PseudoFixtureDef:
def _get_fixturestack(self): def _get_fixturestack(self):
current = self current = self
l = [] values = []
while 1: while 1:
fixturedef = getattr(current, "_fixturedef", None) fixturedef = getattr(current, "_fixturedef", None)
if fixturedef is None: if fixturedef is None:
l.reverse() values.reverse()
return l return values
l.append(fixturedef) values.append(fixturedef)
current = current._parent_request current = current._parent_request
def _getfixturevalue(self, fixturedef): def _getfixturevalue(self, fixturedef):
@ -508,7 +498,7 @@ def _getfixturevalue(self, fixturedef):
source_lineno, source_lineno,
) )
) )
pytest.fail(msg) fail(msg)
else: else:
# indices might not be set if old-style metafunc.addcall() was used # indices might not be set if old-style metafunc.addcall() was used
param_index = funcitem.callspec.indices.get(argname, 0) param_index = funcitem.callspec.indices.get(argname, 0)
@ -541,11 +531,11 @@ def _check_scope(self, argname, invoking_scope, requested_scope):
if scopemismatch(invoking_scope, requested_scope): if scopemismatch(invoking_scope, requested_scope):
# try to report something helpful # try to report something helpful
lines = self._factorytraceback() lines = self._factorytraceback()
pytest.fail("ScopeMismatch: You tried to access the %r scoped " fail("ScopeMismatch: You tried to access the %r scoped "
"fixture %r with a %r scoped request object, " "fixture %r with a %r scoped request object, "
"involved factories\n%s" %( "involved factories\n%s" % (
(requested_scope, argname, invoking_scope, "\n".join(lines))), (requested_scope, argname, invoking_scope, "\n".join(lines))),
pytrace=False) pytrace=False)
def _factorytraceback(self): def _factorytraceback(self):
lines = [] lines = []
@ -554,7 +544,7 @@ def _factorytraceback(self):
fs, lineno = getfslineno(factory) fs, lineno = getfslineno(factory)
p = self._pyfuncitem.session.fspath.bestrelpath(fs) p = self._pyfuncitem.session.fspath.bestrelpath(fs)
args = _format_args(factory) args = _format_args(factory)
lines.append("%s:%d: def %s%s" %( lines.append("%s:%d: def %s%s" % (
p, lineno, factory.__name__, args)) p, lineno, factory.__name__, args))
return lines return lines
@ -570,12 +560,13 @@ def _getscopeitem(self, scope):
return node return node
def __repr__(self): def __repr__(self):
return "<FixtureRequest for %r>" %(self.node) return "<FixtureRequest for %r>" % (self.node)
class SubRequest(FixtureRequest): class SubRequest(FixtureRequest):
""" a sub request for handling getting a fixture from a """ a sub request for handling getting a fixture from a
test function/fixture. """ test function/fixture. """
def __init__(self, request, scope, param, param_index, fixturedef): def __init__(self, request, scope, param, param_index, fixturedef):
self._parent_request = request self._parent_request = request
self.fixturename = fixturedef.argname self.fixturename = fixturedef.argname
@ -584,9 +575,8 @@ def __init__(self, request, scope, param, param_index, fixturedef):
self.param_index = param_index self.param_index = param_index
self.scope = scope self.scope = scope
self._fixturedef = fixturedef self._fixturedef = fixturedef
self.addfinalizer = fixturedef.addfinalizer
self._pyfuncitem = request._pyfuncitem self._pyfuncitem = request._pyfuncitem
self._fixture_values = request._fixture_values self._fixture_values = request._fixture_values
self._fixture_defs = request._fixture_defs self._fixture_defs = request._fixture_defs
self._arg2fixturedefs = request._arg2fixturedefs self._arg2fixturedefs = request._arg2fixturedefs
self._arg2index = request._arg2index self._arg2index = request._arg2index
@ -595,6 +585,9 @@ def __init__(self, request, scope, param, param_index, fixturedef):
def __repr__(self): def __repr__(self):
return "<SubRequest %r for %r>" % (self.fixturename, self._pyfuncitem) return "<SubRequest %r for %r>" % (self.fixturename, self._pyfuncitem)
def addfinalizer(self, finalizer):
self._fixturedef.addfinalizer(finalizer)
class ScopeMismatchError(Exception): class ScopeMismatchError(Exception):
""" A fixture function tries to use a different fixture function which """ A fixture function tries to use a different fixture function which
@ -626,6 +619,7 @@ def scope2index(scope, descr, where=None):
class FixtureLookupError(LookupError): class FixtureLookupError(LookupError):
""" could not return a requested Fixture (missing or invalid). """ """ could not return a requested Fixture (missing or invalid). """
def __init__(self, argname, request, msg=None): def __init__(self, argname, request, msg=None):
self.argname = argname self.argname = argname
self.request = request self.request = request
@ -648,9 +642,9 @@ def formatrepr(self):
lines, _ = inspect.getsourcelines(get_real_func(function)) lines, _ = inspect.getsourcelines(get_real_func(function))
except (IOError, IndexError, TypeError): except (IOError, IndexError, TypeError):
error_msg = "file %s, line %s: source code not available" error_msg = "file %s, line %s: source code not available"
addline(error_msg % (fspath, lineno+1)) addline(error_msg % (fspath, lineno + 1))
else: else:
addline("file %s, line %s" % (fspath, lineno+1)) addline("file %s, line %s" % (fspath, lineno + 1))
for i, line in enumerate(lines): for i, line in enumerate(lines):
line = line.rstrip() line = line.rstrip()
addline(" " + line) addline(" " + line)
@ -666,7 +660,7 @@ def formatrepr(self):
if faclist and name not in available: if faclist and name not in available:
available.append(name) available.append(name)
msg = "fixture %r not found" % (self.argname,) msg = "fixture %r not found" % (self.argname,)
msg += "\n available fixtures: %s" %(", ".join(sorted(available)),) msg += "\n available fixtures: %s" % (", ".join(sorted(available)),)
msg += "\n use 'pytest --fixtures [testpath]' for help on them." msg += "\n use 'pytest --fixtures [testpath]' for help on them."
return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname) return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
@ -692,15 +686,16 @@ def toterminal(self, tw):
tw.line('{0} {1}'.format(FormattedExcinfo.flow_marker, tw.line('{0} {1}'.format(FormattedExcinfo.flow_marker,
line.strip()), red=True) line.strip()), red=True)
tw.line() tw.line()
tw.line("%s:%d" % (self.filename, self.firstlineno+1)) tw.line("%s:%d" % (self.filename, self.firstlineno + 1))
def fail_fixturefunc(fixturefunc, msg): def fail_fixturefunc(fixturefunc, msg):
fs, lineno = getfslineno(fixturefunc) fs, lineno = getfslineno(fixturefunc)
location = "%s:%s" % (fs, lineno+1) location = "%s:%s" % (fs, lineno + 1)
source = _pytest._code.Source(fixturefunc) source = _pytest._code.Source(fixturefunc)
pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, fail(msg + ":\n\n" + str(source.indent()) + "\n" + location,
pytrace=False) pytrace=False)
def call_fixture_func(fixturefunc, request, kwargs): def call_fixture_func(fixturefunc, request, kwargs):
yieldctx = is_generator(fixturefunc) yieldctx = is_generator(fixturefunc)
@ -715,7 +710,7 @@ def teardown():
pass pass
else: else:
fail_fixturefunc(fixturefunc, fail_fixturefunc(fixturefunc,
"yield_fixture function has more than one 'yield'") "yield_fixture function has more than one 'yield'")
request.addfinalizer(teardown) request.addfinalizer(teardown)
else: else:
@ -725,6 +720,7 @@ def teardown():
class FixtureDef: class FixtureDef:
""" A container for a factory definition. """ """ A container for a factory definition. """
def __init__(self, fixturemanager, baseid, argname, func, scope, params, def __init__(self, fixturemanager, baseid, argname, func, scope, params,
unittest=False, ids=None): unittest=False, ids=None):
self._fixturemanager = fixturemanager self._fixturemanager = fixturemanager
@ -749,10 +745,19 @@ def addfinalizer(self, finalizer):
self._finalizer.append(finalizer) self._finalizer.append(finalizer)
def finish(self): def finish(self):
exceptions = []
try: try:
while self._finalizer: while self._finalizer:
func = self._finalizer.pop() try:
func() func = self._finalizer.pop()
func()
except: # noqa
exceptions.append(sys.exc_info())
if exceptions:
e = exceptions[0]
del exceptions # ensure we don't keep all frames alive because of the traceback
py.builtin._reraise(*e)
finally: finally:
ihook = self._fixturemanager.session.ihook ihook = self._fixturemanager.session.ihook
ihook.pytest_fixture_post_finalizer(fixturedef=self) ihook.pytest_fixture_post_finalizer(fixturedef=self)
@ -790,6 +795,7 @@ def __repr__(self):
return ("<FixtureDef name=%r scope=%r baseid=%r >" % return ("<FixtureDef name=%r scope=%r baseid=%r >" %
(self.argname, self.scope, self.baseid)) (self.argname, self.scope, self.baseid))
def pytest_fixture_setup(fixturedef, request): def pytest_fixture_setup(fixturedef, request):
""" Execution of fixture setup. """ """ Execution of fixture setup. """
kwargs = {} kwargs = {}
@ -815,7 +821,7 @@ def pytest_fixture_setup(fixturedef, request):
my_cache_key = request.param_index my_cache_key = request.param_index
try: try:
result = call_fixture_func(fixturefunc, request, kwargs) result = call_fixture_func(fixturefunc, request, kwargs)
except Exception: except TEST_OUTCOME:
fixturedef.cached_result = (None, my_cache_key, sys.exc_info()) fixturedef.cached_result = (None, my_cache_key, sys.exc_info())
raise raise
fixturedef.cached_result = (result, my_cache_key, None) fixturedef.cached_result = (result, my_cache_key, None)
@ -833,17 +839,16 @@ def __init__(self, scope, params, autouse=False, ids=None, name=None):
def __call__(self, function): def __call__(self, function):
if isclass(function): if isclass(function):
raise ValueError( raise ValueError(
"class fixtures not supported (may be in the future)") "class fixtures not supported (may be in the future)")
function._pytestfixturefunction = self function._pytestfixturefunction = self
return function return function
def fixture(scope="function", params=None, autouse=False, ids=None, name=None): def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
""" (return a) decorator to mark a fixture factory function. """ (return a) decorator to mark a fixture factory function.
This decorator can be used (with or or without parameters) to define This decorator can be used (with or without parameters) to define a
a fixture function. The name of the fixture function can later be fixture function. The name of the fixture function can later be
referenced to cause its invocation ahead of running tests: test referenced to cause its invocation ahead of running tests: test
modules or classes can use the pytest.mark.usefixtures(fixturename) modules or classes can use the pytest.mark.usefixtures(fixturename)
marker. Test functions can directly use fixture names as input marker. Test functions can directly use fixture names as input
@ -862,25 +867,25 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
reference is needed to activate the fixture. reference is needed to activate the fixture.
:arg ids: list of string ids each corresponding to the params :arg ids: list of string ids each corresponding to the params
so that they are part of the test id. If no ids are provided so that they are part of the test id. If no ids are provided
they will be generated automatically from the params. they will be generated automatically from the params.
:arg name: the name of the fixture. This defaults to the name of the :arg name: the name of the fixture. This defaults to the name of the
decorated function. If a fixture is used in the same module in decorated function. If a fixture is used in the same module in
which it is defined, the function name of the fixture will be which it is defined, the function name of the fixture will be
shadowed by the function arg that requests the fixture; one way shadowed by the function arg that requests the fixture; one way
to resolve this is to name the decorated function to resolve this is to name the decorated function
``fixture_<fixturename>`` and then use ``fixture_<fixturename>`` and then use
``@pytest.fixture(name='<fixturename>')``. ``@pytest.fixture(name='<fixturename>')``.
Fixtures can optionally provide their values to test functions using a ``yield`` statement, Fixtures can optionally provide their values to test functions using a ``yield`` statement,
instead of ``return``. In this case, the code block after the ``yield`` statement is executed instead of ``return``. In this case, the code block after the ``yield`` statement is executed
as teardown code regardless of the test outcome. A fixture function must yield exactly once. as teardown code regardless of the test outcome. A fixture function must yield exactly once.
""" """
if callable(scope) and params is None and autouse == False: if callable(scope) and params is None and autouse is False:
# direct decoration # direct decoration
return FixtureFunctionMarker( return FixtureFunctionMarker(
"function", params, autouse, name=name)(scope) "function", params, autouse, name=name)(scope)
if params is not None and not isinstance(params, (list, tuple)): if params is not None and not isinstance(params, (list, tuple)):
params = list(params) params = list(params)
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
@ -895,7 +900,7 @@ def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=N
if callable(scope) and params is None and not autouse: if callable(scope) and params is None and not autouse:
# direct decoration # direct decoration
return FixtureFunctionMarker( return FixtureFunctionMarker(
"function", params, autouse, ids=ids, name=name)(scope) "function", params, autouse, ids=ids, name=name)(scope)
else: else:
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
@ -954,14 +959,9 @@ def __init__(self, session):
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
session.config.pluginmanager.register(self, "funcmanage") session.config.pluginmanager.register(self, "funcmanage")
def getfixtureinfo(self, node, func, cls, funcargs=True): def getfixtureinfo(self, node, func, cls, funcargs=True):
if funcargs and not hasattr(node, "nofuncargs"): if funcargs and not hasattr(node, "nofuncargs"):
if cls is not None: argnames = getfuncargnames(func, cls=cls)
startindex = 1
else:
startindex = None
argnames = getfuncargnames(func, startindex)
else: else:
argnames = () argnames = ()
usefixtures = getattr(func, "usefixtures", None) usefixtures = getattr(func, "usefixtures", None)
@ -985,8 +985,8 @@ def pytest_plugin_registered(self, plugin):
# by their test id) # by their test id)
if p.basename.startswith("conftest.py"): if p.basename.startswith("conftest.py"):
nodeid = p.dirpath().relto(self.config.rootdir) nodeid = p.dirpath().relto(self.config.rootdir)
if p.sep != "/": if p.sep != nodes.SEP:
nodeid = nodeid.replace(p.sep, "/") nodeid = nodeid.replace(p.sep, nodes.SEP)
self.parsefactories(plugin, nodeid) self.parsefactories(plugin, nodeid)
def _getautousenames(self, nodeid): def _getautousenames(self, nodeid):
@ -996,7 +996,7 @@ def _getautousenames(self, nodeid):
if nodeid.startswith(baseid): if nodeid.startswith(baseid):
if baseid: if baseid:
i = len(baseid) i = len(baseid)
nextchar = nodeid[i:i+1] nextchar = nodeid[i:i + 1]
if nextchar and nextchar not in ":/": if nextchar and nextchar not in ":/":
continue continue
autousenames.extend(basenames) autousenames.extend(basenames)
@ -1041,9 +1041,14 @@ def pytest_generate_tests(self, metafunc):
if faclist: if faclist:
fixturedef = faclist[-1] fixturedef = faclist[-1]
if fixturedef.params is not None: if fixturedef.params is not None:
func_params = getattr(getattr(metafunc.function, 'parametrize', None), 'args', [[None]]) parametrize_func = getattr(metafunc.function, 'parametrize', None)
func_params = getattr(parametrize_func, 'args', [[None]])
func_kwargs = getattr(parametrize_func, 'kwargs', {})
# skip directly parametrized arguments # skip directly parametrized arguments
argnames = func_params[0] if "argnames" in func_kwargs:
argnames = parametrize_func.kwargs["argnames"]
else:
argnames = func_params[0]
if not isinstance(argnames, (tuple, list)): if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()] argnames = [x.strip() for x in argnames.split(",") if x.strip()]
if argname not in func_params and argname not in argnames: if argname not in func_params and argname not in argnames:
@ -1068,7 +1073,9 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
self._holderobjseen.add(holderobj) self._holderobjseen.add(holderobj)
autousenames = [] autousenames = []
for name in dir(holderobj): for name in dir(holderobj):
obj = getattr(holderobj, name, None) # The attribute can be an arbitrary descriptor, so the attribute
# access below can raise. safe_getatt() ignores such exceptions.
obj = safe_getattr(holderobj, name, None)
# fixture functions have a pytest_funcarg__ prefix (pre-2.3 style) # fixture functions have a pytest_funcarg__ prefix (pre-2.3 style)
# or are "@pytest.fixture" marked # or are "@pytest.fixture" marked
marker = getfixturemarker(obj) marker = getfixturemarker(obj)
@ -1079,7 +1086,7 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
continue continue
marker = defaultfuncargprefixmarker marker = defaultfuncargprefixmarker
from _pytest import deprecated from _pytest import deprecated
self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name)) self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid)
name = name[len(self._argprefix):] name = name[len(self._argprefix):]
elif not isinstance(marker, FixtureFunctionMarker): elif not isinstance(marker, FixtureFunctionMarker):
# magic globals with __getattr__ might have got us a wrong # magic globals with __getattr__ might have got us a wrong
@ -1129,6 +1136,5 @@ def getfixturedefs(self, argname, nodeid):
def _matchfactories(self, fixturedefs, nodeid): def _matchfactories(self, fixturedefs, nodeid):
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
if nodeid.startswith(fixturedef.baseid): if nodes.ischildnode(fixturedef.baseid, nodeid):
yield fixturedef yield fixturedef

View File

@ -2,9 +2,7 @@
Provides a function to report all internal modules for using freezing tools Provides a function to report all internal modules for using freezing tools
pytest pytest
""" """
from __future__ import absolute_import, division, print_function
def pytest_namespace():
return {'freeze_includes': freeze_includes}
def freeze_includes(): def freeze_includes():

View File

@ -1,25 +1,61 @@
""" version info, help messages, tracing configuration. """ """ version info, help messages, tracing configuration. """
from __future__ import absolute_import, division, print_function
import py import py
import pytest import pytest
import os, sys from _pytest.config import PrintHelp
import os
import sys
from argparse import Action
class HelpAction(Action):
"""This is an argparse Action that will raise an exception in
order to skip the rest of the argument parsing when --help is passed.
This prevents argparse from quitting due to missing required arguments
when any are defined, for example by ``pytest_addoption``.
This is similar to the way that the builtin argparse --help option is
implemented by raising SystemExit.
"""
def __init__(self,
option_strings,
dest=None,
default=False,
help=None):
super(HelpAction, self).__init__(
option_strings=option_strings,
dest=dest,
const=True,
default=default,
nargs=0,
help=help)
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, self.const)
# We should only skip the rest of the parsing after preparse is done
if getattr(parser._parser, 'after_preparse', False):
raise PrintHelp
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup('debugconfig') group = parser.getgroup('debugconfig')
group.addoption('--version', action="store_true", group.addoption('--version', action="store_true",
help="display pytest lib version and import information.") help="display pytest lib version and import information.")
group._addoption("-h", "--help", action="store_true", dest="help", group._addoption("-h", "--help", action=HelpAction, dest="help",
help="show help message and configuration info") help="show help message and configuration info")
group._addoption('-p', action="append", dest="plugins", default = [], group._addoption('-p', action="append", dest="plugins", default=[],
metavar="name", metavar="name",
help="early-load given plugin (multi-allowed). " help="early-load given plugin (multi-allowed). "
"To avoid loading of plugins, use the `no:` prefix, e.g. " "To avoid loading of plugins, use the `no:` prefix, e.g. "
"`no:doctest`.") "`no:doctest`.")
group.addoption('--traceconfig', '--trace-config', group.addoption('--traceconfig', '--trace-config',
action="store_true", default=False, action="store_true", default=False,
help="trace considerations of conftest.py files."), help="trace considerations of conftest.py files."),
group.addoption('--debug', group.addoption('--debug',
action="store_true", dest="debug", default=False, action="store_true", dest="debug", default=False,
help="store internal tracing debug information in 'pytestdebug.log'.") help="store internal tracing debug information in 'pytestdebug.log'.")
group._addoption( group._addoption(
'-o', '--override-ini', nargs='*', dest="override_ini", '-o', '--override-ini', nargs='*', dest="override_ini",
action="append", action="append",
@ -34,10 +70,10 @@ def pytest_cmdline_parse():
path = os.path.abspath("pytestdebug.log") path = os.path.abspath("pytestdebug.log")
debugfile = open(path, 'w') debugfile = open(path, 'w')
debugfile.write("versions pytest-%s, py-%s, " debugfile.write("versions pytest-%s, py-%s, "
"python-%s\ncwd=%s\nargs=%s\n\n" %( "python-%s\ncwd=%s\nargs=%s\n\n" % (
pytest.__version__, py.__version__, pytest.__version__, py.__version__,
".".join(map(str, sys.version_info)), ".".join(map(str, sys.version_info)),
os.getcwd(), config._origargs)) os.getcwd(), config._origargs))
config.trace.root.setwriter(debugfile.write) config.trace.root.setwriter(debugfile.write)
undo_tracing = config.pluginmanager.enable_tracing() undo_tracing = config.pluginmanager.enable_tracing()
sys.stderr.write("writing pytestdebug information to %s\n" % path) sys.stderr.write("writing pytestdebug information to %s\n" % path)
@ -51,11 +87,12 @@ def unset_tracing():
config.add_cleanup(unset_tracing) config.add_cleanup(unset_tracing)
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
if config.option.version: if config.option.version:
p = py.path.local(pytest.__file__) p = py.path.local(pytest.__file__)
sys.stderr.write("This is pytest version %s, imported from %s\n" % sys.stderr.write("This is pytest version %s, imported from %s\n" %
(pytest.__version__, p)) (pytest.__version__, p))
plugininfo = getpluginversioninfo(config) plugininfo = getpluginversioninfo(config)
if plugininfo: if plugininfo:
for line in plugininfo: for line in plugininfo:
@ -67,6 +104,7 @@ def pytest_cmdline_main(config):
config._ensure_unconfigure() config._ensure_unconfigure()
return 0 return 0
def showhelp(config): def showhelp(config):
reporter = config.pluginmanager.get_plugin('terminalreporter') reporter = config.pluginmanager.get_plugin('terminalreporter')
tw = reporter._tw tw = reporter._tw
@ -82,7 +120,7 @@ def showhelp(config):
if type is None: if type is None:
type = "string" type = "string"
spec = "%s (%s)" % (name, type) spec = "%s (%s)" % (name, type)
line = " %-24s %s" %(spec, help) line = " %-24s %s" % (spec, help)
tw.line(line[:tw.fullwidth]) tw.line(line[:tw.fullwidth])
tw.line() tw.line()
@ -111,6 +149,7 @@ def showhelp(config):
('pytest_plugins', 'list of plugin names to load'), ('pytest_plugins', 'list of plugin names to load'),
] ]
def getpluginversioninfo(config): def getpluginversioninfo(config):
lines = [] lines = []
plugininfo = config.pluginmanager.list_plugin_distinfo() plugininfo = config.pluginmanager.list_plugin_distinfo()
@ -122,11 +161,12 @@ def getpluginversioninfo(config):
lines.append(" " + content) lines.append(" " + content)
return lines return lines
def pytest_report_header(config): def pytest_report_header(config):
lines = [] lines = []
if config.option.debug or config.option.traceconfig: if config.option.debug or config.option.traceconfig:
lines.append("using: pytest-%s pylib-%s" % lines.append("using: pytest-%s pylib-%s" %
(pytest.__version__,py.__version__)) (pytest.__version__, py.__version__))
verinfo = getpluginversioninfo(config) verinfo = getpluginversioninfo(config)
if verinfo: if verinfo:
@ -140,5 +180,5 @@ def pytest_report_header(config):
r = plugin.__file__ r = plugin.__file__
else: else:
r = repr(plugin) r = repr(plugin)
lines.append(" %-20s: %s" %(name, r)) lines.append(" %-20s: %s" % (name, r))
return lines return lines

View File

@ -8,6 +8,7 @@
# Initialization hooks called for every plugin # Initialization hooks called for every plugin
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec(historic=True) @hookspec(historic=True)
def pytest_addhooks(pluginmanager): def pytest_addhooks(pluginmanager):
"""called at plugin registration time to allow adding new hooks via a call to """called at plugin registration time to allow adding new hooks via a call to
@ -16,11 +17,14 @@ def pytest_addhooks(pluginmanager):
@hookspec(historic=True) @hookspec(historic=True)
def pytest_namespace(): def pytest_namespace():
"""return dict of name->object to be made globally available in """
DEPRECATED: this hook causes direct monkeypatching on pytest, its use is strongly discouraged
return dict of name->object to be made globally available in
the pytest namespace. This hook is called at plugin registration the pytest namespace. This hook is called at plugin registration
time. time.
""" """
@hookspec(historic=True) @hookspec(historic=True)
def pytest_plugin_registered(plugin, manager): def pytest_plugin_registered(plugin, manager):
""" a new pytest plugin got registered. """ """ a new pytest plugin got registered. """
@ -56,11 +60,20 @@ def pytest_addoption(parser):
via (deprecated) ``pytest.config``. via (deprecated) ``pytest.config``.
""" """
@hookspec(historic=True) @hookspec(historic=True)
def pytest_configure(config): def pytest_configure(config):
""" called after command line options have been parsed """
and all plugins and initial conftest files been loaded. Allows plugins and conftest files to perform initial configuration.
This hook is called for every plugin.
This hook is called for every plugin and initial conftest file
after command line options have been parsed.
After that, the hook is called for other conftest files as they are
imported.
:arg config: pytest config object
:type config: _pytest.config.Config
""" """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@ -69,17 +82,25 @@ def pytest_configure(config):
# discoverable conftest.py local plugins. # discoverable conftest.py local plugins.
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_cmdline_parse(pluginmanager, args): def pytest_cmdline_parse(pluginmanager, args):
"""return initialized config object, parsing the specified args. """ """return initialized config object, parsing the specified args.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_cmdline_preparse(config, args): def pytest_cmdline_preparse(config, args):
"""(deprecated) modify command line arguments before option parsing. """ """(deprecated) modify command line arguments before option parsing. """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
""" called for performing the main command line action. The default """ called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop. """ implementation will invoke the configure hooks and runtest_mainloop.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_load_initial_conftests(early_config, parser, args): def pytest_load_initial_conftests(early_config, parser, args):
""" implements the loading of initial conftest files ahead """ implements the loading of initial conftest files ahead
@ -92,88 +113,124 @@ def pytest_load_initial_conftests(early_config, parser, args):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_collection(session): def pytest_collection(session):
""" perform the collection protocol for the given session. """ """ perform the collection protocol for the given session.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_collection_modifyitems(session, config, items): def pytest_collection_modifyitems(session, config, items):
""" called after collection has been performed, may filter or re-order """ called after collection has been performed, may filter or re-order
the items in-place.""" the items in-place."""
def pytest_collection_finish(session): def pytest_collection_finish(session):
""" called after collection has been performed and modified. """ """ called after collection has been performed and modified. """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_ignore_collect(path, config): def pytest_ignore_collect(path, config):
""" return True to prevent considering this path for collection. """ return True to prevent considering this path for collection.
This hook is consulted for all files and directories prior to calling This hook is consulted for all files and directories prior to calling
more specific hooks. more specific hooks.
Stops at first non-None result, see :ref:`firstresult`
""" """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_collect_directory(path, parent): def pytest_collect_directory(path, parent):
""" called before traversing a directory for collection files. """ """ called before traversing a directory for collection files.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_collect_file(path, parent): def pytest_collect_file(path, parent):
""" return collection Node or None for the given path. Any new node """ return collection Node or None for the given path. Any new node
needs to have the specified ``parent`` as a parent.""" needs to have the specified ``parent`` as a parent."""
# logging hooks for collection # logging hooks for collection
def pytest_collectstart(collector): def pytest_collectstart(collector):
""" collector starts collecting. """ """ collector starts collecting. """
def pytest_itemcollected(item): def pytest_itemcollected(item):
""" we just collected a test item. """ """ we just collected a test item. """
def pytest_collectreport(report): def pytest_collectreport(report):
""" collector finished collecting. """ """ collector finished collecting. """
def pytest_deselected(items): def pytest_deselected(items):
""" called for test items deselected by keyword. """ """ called for test items deselected by keyword. """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_make_collect_report(collector): def pytest_make_collect_report(collector):
""" perform ``collector.collect()`` and return a CollectReport. """ """ perform ``collector.collect()`` and return a CollectReport.
Stops at first non-None result, see :ref:`firstresult` """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Python test function related hooks # Python test function related hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_pycollect_makemodule(path, parent): def pytest_pycollect_makemodule(path, parent):
""" return a Module collector or None for the given path. """ return a Module collector or None for the given path.
This hook will be called for each matching test module path. This hook will be called for each matching test module path.
The pytest_collect_file hook needs to be used if you want to The pytest_collect_file hook needs to be used if you want to
create test modules for files that do not match as a test module. create test modules for files that do not match as a test module.
"""
Stops at first non-None result, see :ref:`firstresult` """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_pycollect_makeitem(collector, name, obj): def pytest_pycollect_makeitem(collector, name, obj):
""" return custom item/collector for a python object in a module, or None. """ """ return custom item/collector for a python object in a module, or None.
Stops at first non-None result, see :ref:`firstresult` """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_pyfunc_call(pyfuncitem): def pytest_pyfunc_call(pyfuncitem):
""" call underlying test function. """ """ call underlying test function.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
""" generate (multiple) parametrized calls to a test function.""" """ generate (multiple) parametrized calls to a test function."""
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_make_parametrize_id(config, val): def pytest_make_parametrize_id(config, val, argname):
"""Return a user-friendly string representation of the given ``val`` that will be used """Return a user-friendly string representation of the given ``val`` that will be used
by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
""" The parameter name is available as ``argname``, if required.
Stops at first non-None result, see :ref:`firstresult` """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# generic runtest related hooks # generic runtest related hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_runtestloop(session): def pytest_runtestloop(session):
""" called for performing the main runtest loop """ called for performing the main runtest loop
(after collection finished). """ (after collection finished).
Stops at first non-None result, see :ref:`firstresult` """
def pytest_itemstart(item, node): def pytest_itemstart(item, node):
""" (deprecated, use pytest_runtest_logstart). """ """ (deprecated, use pytest_runtest_logstart). """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_runtest_protocol(item, nextitem): def pytest_runtest_protocol(item, nextitem):
""" implements the runtest_setup/call/teardown protocol for """ implements the runtest_setup/call/teardown protocol for
@ -187,17 +244,23 @@ def pytest_runtest_protocol(item, nextitem):
:py:func:`pytest_runtest_teardown`. :py:func:`pytest_runtest_teardown`.
:return boolean: True if no further hook implementations should be invoked. :return boolean: True if no further hook implementations should be invoked.
"""
Stops at first non-None result, see :ref:`firstresult` """
def pytest_runtest_logstart(nodeid, location): def pytest_runtest_logstart(nodeid, location):
""" signal the start of running a single test item. """ """ signal the start of running a single test item. """
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
""" called before ``pytest_runtest_call(item)``. """ """ called before ``pytest_runtest_call(item)``. """
def pytest_runtest_call(item): def pytest_runtest_call(item):
""" called to execute the test ``item``. """ """ called to execute the test ``item``. """
def pytest_runtest_teardown(item, nextitem): def pytest_runtest_teardown(item, nextitem):
""" called after ``pytest_runtest_call``. """ called after ``pytest_runtest_call``.
@ -207,12 +270,15 @@ def pytest_runtest_teardown(item, nextitem):
so that nextitem only needs to call setup-functions. so that nextitem only needs to call setup-functions.
""" """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
""" return a :py:class:`_pytest.runner.TestReport` object """ return a :py:class:`_pytest.runner.TestReport` object
for the given :py:class:`pytest.Item` and for the given :py:class:`pytest.Item <_pytest.main.Item>` and
:py:class:`_pytest.runner.CallInfo`. :py:class:`_pytest.runner.CallInfo`.
"""
Stops at first non-None result, see :ref:`firstresult` """
def pytest_runtest_logreport(report): def pytest_runtest_logreport(report):
""" process a test setup/call/teardown report relating to """ process a test setup/call/teardown report relating to
@ -222,9 +288,13 @@ def pytest_runtest_logreport(report):
# Fixture related hooks # Fixture related hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_fixture_setup(fixturedef, request): def pytest_fixture_setup(fixturedef, request):
""" performs fixture setup execution. """ """ performs fixture setup execution.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_fixture_post_finalizer(fixturedef): def pytest_fixture_post_finalizer(fixturedef):
""" called after fixture teardown, but before the cache is cleared so """ called after fixture teardown, but before the cache is cleared so
@ -235,18 +305,21 @@ def pytest_fixture_post_finalizer(fixturedef):
# test session related hooks # test session related hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
def pytest_sessionstart(session): def pytest_sessionstart(session):
""" before session.main() is called. """ """ before session.main() is called. """
def pytest_sessionfinish(session, exitstatus): def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """ """ whole test run finishes. """
def pytest_unconfigure(config): def pytest_unconfigure(config):
""" called before test process is exited. """ """ called before test process is exited. """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# hooks for customising the assert methods # hooks for customizing the assert methods
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
def pytest_assertrepr_compare(config, op, left, right): def pytest_assertrepr_compare(config, op, left, right):
@ -255,19 +328,48 @@ def pytest_assertrepr_compare(config, op, left, right):
Return None for no custom explanation, otherwise return a list Return None for no custom explanation, otherwise return a list
of strings. The strings will be joined by newlines but any newlines of strings. The strings will be joined by newlines but any newlines
*in* a string will be escaped. Note that all but the first line will *in* a string will be escaped. Note that all but the first line will
be indented sligthly, the intention is for the first line to be a summary. be indented slightly, the intention is for the first line to be a summary.
""" """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# hooks for influencing reporting (invoked from _pytest_terminal) # hooks for influencing reporting (invoked from _pytest_terminal)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
def pytest_report_header(config, startdir): def pytest_report_header(config, startdir):
""" return a string to be displayed as header info for terminal reporting.""" """ return a string or list of strings to be displayed as header info for terminal reporting.
:param config: the pytest config object.
:param startdir: py.path object with the starting dir
.. note::
This function should be implemented only in plugins or ``conftest.py``
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
"""
def pytest_report_collectionfinish(config, startdir, items):
"""
.. versionadded:: 3.2
return a string or list of strings to be displayed after collection has finished successfully.
This strings will be displayed after the standard "collected X items" message.
:param config: the pytest config object.
:param startdir: py.path object with the starting dir
:param items: list of pytest items that are going to be executed; this list should not be modified.
"""
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_report_teststatus(report): def pytest_report_teststatus(report):
""" return result-category, shortletter and verbose word for reporting.""" """ return result-category, shortletter and verbose word for reporting.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_terminal_summary(terminalreporter, exitstatus): def pytest_terminal_summary(terminalreporter, exitstatus):
""" add additional section in terminal summary reporting. """ """ add additional section in terminal summary reporting. """
@ -283,20 +385,26 @@ def pytest_logwarning(message, code, nodeid, fslocation):
# doctest hooks # doctest hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_doctest_prepare_content(content): def pytest_doctest_prepare_content(content):
""" return processed content for a given doctest""" """ return processed content for a given doctest
Stops at first non-None result, see :ref:`firstresult` """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# error handling and internal debugging hooks # error handling and internal debugging hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
def pytest_internalerror(excrepr, excinfo): def pytest_internalerror(excrepr, excinfo):
""" called for internal errors. """ """ called for internal errors. """
def pytest_keyboard_interrupt(excinfo): def pytest_keyboard_interrupt(excinfo):
""" called for keyboard interrupt. """ """ called for keyboard interrupt. """
def pytest_exception_interact(node, call, report): def pytest_exception_interact(node, call, report):
"""called when an exception was raised which can potentially be """called when an exception was raised which can potentially be
interactively handled. interactively handled.
@ -305,6 +413,7 @@ def pytest_exception_interact(node, call, report):
that is not an internal exception like ``skip.Exception``. that is not an internal exception like ``skip.Exception``.
""" """
def pytest_enter_pdb(config): def pytest_enter_pdb(config):
""" called upon pdb.set_trace(), can be used by plugins to take special """ called upon pdb.set_trace(), can be used by plugins to take special
action just before the python debugger enters in interactive mode. action just before the python debugger enters in interactive mode.

View File

@ -4,9 +4,11 @@
Based on initial code from Ross Lawley. Based on initial code from Ross Lawley.
Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/
src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
""" """
# Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/ from __future__ import absolute_import, division, print_function
# src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
import functools import functools
import py import py
@ -15,6 +17,7 @@
import sys import sys
import time import time
import pytest import pytest
from _pytest import nodes
from _pytest.config import filename_arg from _pytest.config import filename_arg
# Python 2.X and 3.X compatibility # Python 2.X and 3.X compatibility
@ -105,6 +108,8 @@ def record_testreport(self, testreport):
} }
if testreport.location[1] is not None: if testreport.location[1] is not None:
attrs["line"] = testreport.location[1] attrs["line"] = testreport.location[1]
if hasattr(testreport, "url"):
attrs["url"] = testreport.url
self.attrs = attrs self.attrs = attrs
def to_xml(self): def to_xml(self):
@ -119,7 +124,7 @@ def _add_simple(self, kind, message, data=None):
node = kind(data, message=message) node = kind(data, message=message)
self.append(node) self.append(node)
def _write_captured_output(self, report): def write_captured_output(self, report):
for capname in ('out', 'err'): for capname in ('out', 'err'):
content = getattr(report, 'capstd' + capname) content = getattr(report, 'capstd' + capname)
if content: if content:
@ -128,7 +133,6 @@ def _write_captured_output(self, report):
def append_pass(self, report): def append_pass(self, report):
self.add_stats('passed') self.add_stats('passed')
self._write_captured_output(report)
def append_failure(self, report): def append_failure(self, report):
# msg = str(report.longrepr.reprtraceback.extraline) # msg = str(report.longrepr.reprtraceback.extraline)
@ -147,7 +151,6 @@ def append_failure(self, report):
fail = Junit.failure(message=message) fail = Junit.failure(message=message)
fail.append(bin_xml_escape(report.longrepr)) fail.append(bin_xml_escape(report.longrepr))
self.append(fail) self.append(fail)
self._write_captured_output(report)
def append_collect_error(self, report): def append_collect_error(self, report):
# msg = str(report.longrepr.reprtraceback.extraline) # msg = str(report.longrepr.reprtraceback.extraline)
@ -165,7 +168,6 @@ def append_error(self, report):
msg = "test setup failure" msg = "test setup failure"
self._add_simple( self._add_simple(
Junit.error, msg, report.longrepr) Junit.error, msg, report.longrepr)
self._write_captured_output(report)
def append_skipped(self, report): def append_skipped(self, report):
if hasattr(report, "wasxfail"): if hasattr(report, "wasxfail"):
@ -180,7 +182,7 @@ def append_skipped(self, report):
Junit.skipped("%s:%s: %s" % (filename, lineno, skipreason), Junit.skipped("%s:%s: %s" % (filename, lineno, skipreason),
type="pytest.skip", type="pytest.skip",
message=skipreason)) message=skipreason))
self._write_captured_output(report) self.write_captured_output(report)
def finalize(self): def finalize(self):
data = self.to_xml().unicode(indent=0) data = self.to_xml().unicode(indent=0)
@ -225,13 +227,14 @@ def pytest_addoption(parser):
metavar="str", metavar="str",
default=None, default=None,
help="prepend prefix to classnames in junit-xml output") help="prepend prefix to classnames in junit-xml output")
parser.addini("junit_suite_name", "Test suite name for JUnit report", default="pytest")
def pytest_configure(config): def pytest_configure(config):
xmlpath = config.option.xmlpath xmlpath = config.option.xmlpath
# prevent opening xmllog on slave nodes (xdist) # prevent opening xmllog on slave nodes (xdist)
if xmlpath and not hasattr(config, 'slaveinput'): if xmlpath and not hasattr(config, 'slaveinput'):
config._xml = LogXML(xmlpath, config.option.junitprefix) config._xml = LogXML(xmlpath, config.option.junitprefix, config.getini("junit_suite_name"))
config.pluginmanager.register(config._xml) config.pluginmanager.register(config._xml)
@ -250,7 +253,7 @@ def mangle_test_address(address):
except ValueError: except ValueError:
pass pass
# convert file path to dotted path # convert file path to dotted path
names[0] = names[0].replace("/", '.') names[0] = names[0].replace(nodes.SEP, '.')
names[0] = _py_ext_re.sub("", names[0]) names[0] = _py_ext_re.sub("", names[0])
# put any params back # put any params back
names[-1] += possible_open_bracket + params names[-1] += possible_open_bracket + params
@ -258,10 +261,11 @@ def mangle_test_address(address):
class LogXML(object): class LogXML(object):
def __init__(self, logfile, prefix): def __init__(self, logfile, prefix, suite_name="pytest"):
logfile = os.path.expanduser(os.path.expandvars(logfile)) logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(os.path.abspath(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile))
self.prefix = prefix self.prefix = prefix
self.suite_name = suite_name
self.stats = dict.fromkeys([ self.stats = dict.fromkeys([
'error', 'error',
'passed', 'passed',
@ -271,6 +275,9 @@ def __init__(self, logfile, prefix):
self.node_reporters = {} # nodeid -> _NodeReporter self.node_reporters = {} # nodeid -> _NodeReporter
self.node_reporters_ordered = [] self.node_reporters_ordered = []
self.global_properties = [] self.global_properties = []
# List of reports that failed on call but teardown is pending.
self.open_reports = []
self.cnt_double_fail_tests = 0
def finalize(self, report): def finalize(self, report):
nodeid = getattr(report, 'nodeid', report) nodeid = getattr(report, 'nodeid', report)
@ -330,14 +337,33 @@ def pytest_runtest_logreport(self, report):
-> teardown node2 -> teardown node2
-> teardown node1 -> teardown node1
""" """
close_report = None
if report.passed: if report.passed:
if report.when == "call": # ignore setup/teardown if report.when == "call": # ignore setup/teardown
reporter = self._opentestcase(report) reporter = self._opentestcase(report)
reporter.append_pass(report) reporter.append_pass(report)
elif report.failed: elif report.failed:
if report.when == "teardown":
# The following vars are needed when xdist plugin is used
report_wid = getattr(report, "worker_id", None)
report_ii = getattr(report, "item_index", None)
close_report = next(
(rep for rep in self.open_reports
if (rep.nodeid == report.nodeid and
getattr(rep, "item_index", None) == report_ii and
getattr(rep, "worker_id", None) == report_wid
)
), None)
if close_report:
# We need to open new testcase in case we have failure in
# call and error in teardown in order to follow junit
# schema
self.finalize(close_report)
self.cnt_double_fail_tests += 1
reporter = self._opentestcase(report) reporter = self._opentestcase(report)
if report.when == "call": if report.when == "call":
reporter.append_failure(report) reporter.append_failure(report)
self.open_reports.append(report)
else: else:
reporter.append_error(report) reporter.append_error(report)
elif report.skipped: elif report.skipped:
@ -345,7 +371,20 @@ def pytest_runtest_logreport(self, report):
reporter.append_skipped(report) reporter.append_skipped(report)
self.update_testcase_duration(report) self.update_testcase_duration(report)
if report.when == "teardown": if report.when == "teardown":
reporter = self._opentestcase(report)
reporter.write_captured_output(report)
self.finalize(report) self.finalize(report)
report_wid = getattr(report, "worker_id", None)
report_ii = getattr(report, "item_index", None)
close_report = next(
(rep for rep in self.open_reports
if (rep.nodeid == report.nodeid and
getattr(rep, "item_index", None) == report_ii and
getattr(rep, "worker_id", None) == report_wid
)
), None)
if close_report:
self.open_reports.remove(close_report)
def update_testcase_duration(self, report): def update_testcase_duration(self, report):
"""accumulates total duration for nodeid from given report and updates """accumulates total duration for nodeid from given report and updates
@ -378,14 +417,15 @@ def pytest_sessionfinish(self):
suite_stop_time = time.time() suite_stop_time = time.time()
suite_time_delta = suite_stop_time - self.suite_start_time suite_time_delta = suite_stop_time - self.suite_start_time
numtests = self.stats['passed'] + self.stats['failure'] + self.stats['skipped'] + self.stats['error'] numtests = (self.stats['passed'] + self.stats['failure'] +
self.stats['skipped'] + self.stats['error'] -
self.cnt_double_fail_tests)
logfile.write('<?xml version="1.0" encoding="utf-8"?>') logfile.write('<?xml version="1.0" encoding="utf-8"?>')
logfile.write(Junit.testsuite( logfile.write(Junit.testsuite(
self._get_global_properties_node(), self._get_global_properties_node(),
[x.to_xml() for x in self.node_reporters_ordered], [x.to_xml() for x in self.node_reporters_ordered],
name="pytest", name=self.suite_name,
errors=self.stats['error'], errors=self.stats['error'],
failures=self.stats['failure'], failures=self.stats['failure'],
skips=self.stats['skipped'], skips=self.stats['skipped'],
@ -405,9 +445,9 @@ def _get_global_properties_node(self):
""" """
if self.global_properties: if self.global_properties:
return Junit.properties( return Junit.properties(
[ [
Junit.property(name=name, value=value) Junit.property(name=name, value=value)
for name, value in self.global_properties for name, value in self.global_properties
] ]
) )
return '' return ''

View File

@ -1,18 +1,21 @@
""" core implementation of testing process: init, session, runtest loop. """ """ core implementation of testing process: init, session, runtest loop. """
from __future__ import absolute_import, division, print_function
import functools import functools
import os import os
import sys import sys
import _pytest import _pytest
from _pytest import nodes
import _pytest._code import _pytest._code
import py import py
import pytest
try: try:
from collections import MutableMapping as MappingMixin from collections import MutableMapping as MappingMixin
except ImportError: except ImportError:
from UserDict import DictMixin as MappingMixin from UserDict import DictMixin as MappingMixin
from _pytest.config import directory_arg from _pytest.config import directory_arg, UsageError, hookimpl
from _pytest.outcomes import exit
from _pytest.runner import collect_one_node from _pytest.runner import collect_one_node
tracebackcutdir = py.path.local(_pytest.__file__).dirpath() tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@ -25,63 +28,73 @@
EXIT_USAGEERROR = 4 EXIT_USAGEERROR = 4
EXIT_NOTESTSCOLLECTED = 5 EXIT_NOTESTSCOLLECTED = 5
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addini("norecursedirs", "directory patterns to avoid for recursion", parser.addini("norecursedirs", "directory patterns to avoid for recursion",
type="args", default=['.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg']) type="args", default=['.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv'])
parser.addini("testpaths", "directories to search for tests when no files or directories are given in the command line.", parser.addini("testpaths", "directories to search for tests when no files or directories are given in the "
type="args", default=[]) "command line.",
#parser.addini("dirpatterns", type="args", default=[])
# parser.addini("dirpatterns",
# "patterns specifying possible locations of test files", # "patterns specifying possible locations of test files",
# type="linelist", default=["**/test_*.txt", # type="linelist", default=["**/test_*.txt",
# "**/test_*.py", "**/*_test.py"] # "**/test_*.py", "**/*_test.py"]
#) # )
group = parser.getgroup("general", "running and selection options") group = parser.getgroup("general", "running and selection options")
group._addoption('-x', '--exitfirst', action="store_const", group._addoption('-x', '--exitfirst', action="store_const",
dest="maxfail", const=1, dest="maxfail", const=1,
help="exit instantly on first error or failed test."), help="exit instantly on first error or failed test."),
group._addoption('--maxfail', metavar="num", group._addoption('--maxfail', metavar="num",
action="store", type=int, dest="maxfail", default=0, action="store", type=int, dest="maxfail", default=0,
help="exit after first num failures or errors.") help="exit after first num failures or errors.")
group._addoption('--strict', action="store_true", group._addoption('--strict', action="store_true",
help="run pytest in strict mode, warnings become errors.") help="marks not registered in configuration file raise errors.")
group._addoption("-c", metavar="file", type=str, dest="inifilename", group._addoption("-c", metavar="file", type=str, dest="inifilename",
help="load configuration from `file` instead of trying to locate one of the implicit configuration files.") help="load configuration from `file` instead of trying to locate one of the implicit "
"configuration files.")
group._addoption("--continue-on-collection-errors", action="store_true", group._addoption("--continue-on-collection-errors", action="store_true",
default=False, dest="continue_on_collection_errors", default=False, dest="continue_on_collection_errors",
help="Force test execution even if collection errors occur.") help="Force test execution even if collection errors occur.")
group = parser.getgroup("collect", "collection") group = parser.getgroup("collect", "collection")
group.addoption('--collectonly', '--collect-only', action="store_true", group.addoption('--collectonly', '--collect-only', action="store_true",
help="only collect tests, don't execute them."), help="only collect tests, don't execute them."),
group.addoption('--pyargs', action="store_true", group.addoption('--pyargs', action="store_true",
help="try to interpret all arguments as python packages.") help="try to interpret all arguments as python packages.")
group.addoption("--ignore", action="append", metavar="path", group.addoption("--ignore", action="append", metavar="path",
help="ignore path during collection (multi-allowed).") help="ignore path during collection (multi-allowed).")
# when changing this to --conf-cut-dir, config.py Conftest.setinitial # when changing this to --conf-cut-dir, config.py Conftest.setinitial
# needs upgrading as well # needs upgrading as well
group.addoption('--confcutdir', dest="confcutdir", default=None, group.addoption('--confcutdir', dest="confcutdir", default=None,
metavar="dir", type=functools.partial(directory_arg, optname="--confcutdir"), metavar="dir", type=functools.partial(directory_arg, optname="--confcutdir"),
help="only load conftest.py's relative to specified dir.") help="only load conftest.py's relative to specified dir.")
group.addoption('--noconftest', action="store_true", group.addoption('--noconftest', action="store_true",
dest="noconftest", default=False, dest="noconftest", default=False,
help="Don't load any conftest.py files.") help="Don't load any conftest.py files.")
group.addoption('--keepduplicates', '--keep-duplicates', action="store_true", group.addoption('--keepduplicates', '--keep-duplicates', action="store_true",
dest="keepduplicates", default=False, dest="keepduplicates", default=False,
help="Keep duplicate tests.") help="Keep duplicate tests.")
group.addoption('--collect-in-virtualenv', action='store_true',
dest='collect_in_virtualenv', default=False,
help="Don't ignore tests in a local virtualenv directory")
group = parser.getgroup("debugconfig", group = parser.getgroup("debugconfig",
"test session debugging and configuration") "test session debugging and configuration")
group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir",
help="base temporary directory for this test run.") help="base temporary directory for this test run.")
def pytest_namespace(): def pytest_namespace():
collect = dict(Item=Item, Collector=Collector, File=File, Session=Session) """keeping this one works around a deeper startup issue in pytest
return dict(collect=collect)
i tried to find it for a while but the amount of time turned unsustainable,
so i put a hack in to revisit later
"""
return {}
def pytest_configure(config): def pytest_configure(config):
pytest.config = config # compatibiltiy __import__('pytest').config = config # compatibiltiy
def wrap_session(config, doit): def wrap_session(config, doit):
@ -96,17 +109,16 @@ def wrap_session(config, doit):
config.hook.pytest_sessionstart(session=session) config.hook.pytest_sessionstart(session=session)
initstate = 2 initstate = 2
session.exitstatus = doit(config, session) or 0 session.exitstatus = doit(config, session) or 0
except pytest.UsageError: except UsageError:
raise raise
except KeyboardInterrupt: except KeyboardInterrupt:
excinfo = _pytest._code.ExceptionInfo() excinfo = _pytest._code.ExceptionInfo()
if initstate < 2 and isinstance( if initstate < 2 and isinstance(excinfo.value, exit.Exception):
excinfo.value, pytest.exit.Exception):
sys.stderr.write('{0}: {1}\n'.format( sys.stderr.write('{0}: {1}\n'.format(
excinfo.typename, excinfo.value.msg)) excinfo.typename, excinfo.value.msg))
config.hook.pytest_keyboard_interrupt(excinfo=excinfo) config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
session.exitstatus = EXIT_INTERRUPTED session.exitstatus = EXIT_INTERRUPTED
except: except: # noqa
excinfo = _pytest._code.ExceptionInfo() excinfo = _pytest._code.ExceptionInfo()
config.notify_exception(excinfo, config.option) config.notify_exception(excinfo, config.option)
session.exitstatus = EXIT_INTERNALERROR session.exitstatus = EXIT_INTERNALERROR
@ -123,9 +135,11 @@ def wrap_session(config, doit):
config._ensure_unconfigure() config._ensure_unconfigure()
return session.exitstatus return session.exitstatus
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
return wrap_session(config, _main) return wrap_session(config, _main)
def _main(config, session): def _main(config, session):
""" default command line protocol for initialization, session, """ default command line protocol for initialization, session,
running tests and reporting. """ running tests and reporting. """
@ -137,9 +151,11 @@ def _main(config, session):
elif session.testscollected == 0: elif session.testscollected == 0:
return EXIT_NOTESTSCOLLECTED return EXIT_NOTESTSCOLLECTED
def pytest_collection(session): def pytest_collection(session):
return session.perform_collect() return session.perform_collect()
def pytest_runtestloop(session): def pytest_runtestloop(session):
if (session.testsfailed and if (session.testsfailed and
not session.config.option.continue_on_collection_errors): not session.config.option.continue_on_collection_errors):
@ -150,21 +166,36 @@ def pytest_runtestloop(session):
return True return True
for i, item in enumerate(session.items): for i, item in enumerate(session.items):
nextitem = session.items[i+1] if i+1 < len(session.items) else None nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
if session.shouldstop: if session.shouldstop:
raise session.Interrupted(session.shouldstop) raise session.Interrupted(session.shouldstop)
return True return True
def _in_venv(path):
"""Attempts to detect if ``path`` is the root of a Virtual Environment by
checking for the existence of the appropriate activate script"""
bindir = path.join('Scripts' if sys.platform.startswith('win') else 'bin')
if not bindir.exists():
return False
activates = ('activate', 'activate.csh', 'activate.fish',
'Activate', 'Activate.bat', 'Activate.ps1')
return any([fname.basename in activates for fname in bindir.listdir()])
def pytest_ignore_collect(path, config): def pytest_ignore_collect(path, config):
p = path.dirpath() ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath())
ignore_paths = config._getconftest_pathlist("collect_ignore", path=p)
ignore_paths = ignore_paths or [] ignore_paths = ignore_paths or []
excludeopt = config.getoption("ignore") excludeopt = config.getoption("ignore")
if excludeopt: if excludeopt:
ignore_paths.extend([py.path.local(x) for x in excludeopt]) ignore_paths.extend([py.path.local(x) for x in excludeopt])
if path in ignore_paths: if py.path.local(path) in ignore_paths:
return True
allow_in_venv = config.getoption("collect_in_virtualenv")
if _in_venv(path) and not allow_in_venv:
return True return True
# Skip duplicate paths. # Skip duplicate paths.
@ -190,14 +221,22 @@ def __getattr__(self, name):
self.__dict__[name] = x self.__dict__[name] = x
return x return x
def compatproperty(name):
def fget(self):
import warnings
warnings.warn("This usage is deprecated, please use pytest.{0} instead".format(name),
PendingDeprecationWarning, stacklevel=2)
return getattr(pytest, name)
return property(fget) class _CompatProperty(object):
def __init__(self, name):
self.name = name
def __get__(self, obj, owner):
if obj is None:
return self
# TODO: reenable in the features branch
# warnings.warn(
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
# name=self.name, owner=type(owner).__name__),
# PendingDeprecationWarning, stacklevel=2)
return getattr(__import__('pytest'), self.name)
class NodeKeywords(MappingMixin): class NodeKeywords(MappingMixin):
def __init__(self, node): def __init__(self, node):
@ -269,24 +308,28 @@ def ihook(self):
""" fspath sensitive hook proxy used to call pytest hooks""" """ fspath sensitive hook proxy used to call pytest hooks"""
return self.session.gethookproxy(self.fspath) return self.session.gethookproxy(self.fspath)
Module = compatproperty("Module") Module = _CompatProperty("Module")
Class = compatproperty("Class") Class = _CompatProperty("Class")
Instance = compatproperty("Instance") Instance = _CompatProperty("Instance")
Function = compatproperty("Function") Function = _CompatProperty("Function")
File = compatproperty("File") File = _CompatProperty("File")
Item = compatproperty("Item") Item = _CompatProperty("Item")
def _getcustomclass(self, name): def _getcustomclass(self, name):
cls = getattr(self, name) maybe_compatprop = getattr(type(self), name)
if cls != getattr(pytest, name): if isinstance(maybe_compatprop, _CompatProperty):
py.log._apiwarn("2.0", "use of node.%s is deprecated, " return getattr(__import__('pytest'), name)
"use pytest_pycollect_makeitem(...) to create custom " else:
"collection nodes" % name) cls = getattr(self, name)
# TODO: reenable in the features branch
# warnings.warn("use of node.%s is deprecated, "
# "use pytest_pycollect_makeitem(...) to create custom "
# "collection nodes" % name, category=DeprecationWarning)
return cls return cls
def __repr__(self): def __repr__(self):
return "<%s %r>" %(self.__class__.__name__, return "<%s %r>" % (self.__class__.__name__,
getattr(self, 'name', None)) getattr(self, 'name', None))
def warn(self, code, message): def warn(self, code, message):
""" generate a warning with the given code and message for this """ generate a warning with the given code and message for this
@ -295,9 +338,6 @@ def warn(self, code, message):
fslocation = getattr(self, "location", None) fslocation = getattr(self, "location", None)
if fslocation is None: if fslocation is None:
fslocation = getattr(self, "fspath", None) fslocation = getattr(self, "fspath", None)
else:
fslocation = "%s:%s" % (fslocation[0], fslocation[1] + 1)
self.ihook.pytest_logwarning.call_historic(kwargs=dict( self.ihook.pytest_logwarning.call_historic(kwargs=dict(
code=code, message=message, code=code, message=message,
nodeid=self.nodeid, fslocation=fslocation)) nodeid=self.nodeid, fslocation=fslocation))
@ -335,7 +375,7 @@ def _memoizedcall(self, attrname, function):
res = function() res = function()
except py.builtin._sysex: except py.builtin._sysex:
raise raise
except: except: # noqa
failure = sys.exc_info() failure = sys.exc_info()
setattr(self, exattrname, failure) setattr(self, exattrname, failure)
raise raise
@ -358,9 +398,9 @@ def add_marker(self, marker):
``marker`` can be a string or pytest.mark.* instance. ``marker`` can be a string or pytest.mark.* instance.
""" """
from _pytest.mark import MarkDecorator from _pytest.mark import MarkDecorator, MARK_GEN
if isinstance(marker, py.builtin._basestring): if isinstance(marker, py.builtin._basestring):
marker = MarkDecorator(marker) marker = getattr(MARK_GEN, marker)
elif not isinstance(marker, MarkDecorator): elif not isinstance(marker, MarkDecorator):
raise ValueError("is not a string or pytest.mark.* Marker") raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker.name] = marker self.keywords[marker.name] = marker
@ -410,7 +450,7 @@ def _repr_failure_py(self, excinfo, style=None):
return excinfo.value.formatrepr() return excinfo.value.formatrepr()
tbfilter = True tbfilter = True
if self.config.option.fulltrace: if self.config.option.fulltrace:
style="long" style = "long"
else: else:
tb = _pytest._code.Traceback([excinfo.traceback[-1]]) tb = _pytest._code.Traceback([excinfo.traceback[-1]])
self._prunetraceback(excinfo) self._prunetraceback(excinfo)
@ -438,6 +478,7 @@ def _repr_failure_py(self, excinfo, style=None):
repr_failure = _repr_failure_py repr_failure = _repr_failure_py
class Collector(Node): class Collector(Node):
""" Collector instances create children through collect() """ Collector instances create children through collect()
and thus iteratively build a tree. and thus iteratively build a tree.
@ -459,10 +500,6 @@ def repr_failure(self, excinfo):
return str(exc.args[0]) return str(exc.args[0])
return self._repr_failure_py(excinfo, style="short") return self._repr_failure_py(excinfo, style="short")
def _memocollect(self):
""" internal helper method to cache results of calling collect(). """
return self._memoizedcall('_collected', lambda: list(self.collect()))
def _prunetraceback(self, excinfo): def _prunetraceback(self, excinfo):
if hasattr(self, 'fspath'): if hasattr(self, 'fspath'):
traceback = excinfo.traceback traceback = excinfo.traceback
@ -471,27 +508,38 @@ def _prunetraceback(self, excinfo):
ntraceback = ntraceback.cut(excludepath=tracebackcutdir) ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
excinfo.traceback = ntraceback.filter() excinfo.traceback = ntraceback.filter()
class FSCollector(Collector): class FSCollector(Collector):
def __init__(self, fspath, parent=None, config=None, session=None): def __init__(self, fspath, parent=None, config=None, session=None):
fspath = py.path.local(fspath) # xxx only for test_resultlog.py? fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
name = fspath.basename name = fspath.basename
if parent is not None: if parent is not None:
rel = fspath.relto(parent.fspath) rel = fspath.relto(parent.fspath)
if rel: if rel:
name = rel name = rel
name = name.replace(os.sep, "/") name = name.replace(os.sep, nodes.SEP)
super(FSCollector, self).__init__(name, parent, config, session) super(FSCollector, self).__init__(name, parent, config, session)
self.fspath = fspath self.fspath = fspath
def _check_initialpaths_for_relpath(self):
for initialpath in self.session._initialpaths:
if self.fspath.common(initialpath) == initialpath:
return self.fspath.relto(initialpath.dirname)
def _makeid(self): def _makeid(self):
relpath = self.fspath.relto(self.config.rootdir) relpath = self.fspath.relto(self.config.rootdir)
if os.sep != "/":
relpath = relpath.replace(os.sep, "/") if not relpath:
relpath = self._check_initialpaths_for_relpath()
if os.sep != nodes.SEP:
relpath = relpath.replace(os.sep, nodes.SEP)
return relpath return relpath
class File(FSCollector): class File(FSCollector):
""" base class for collecting tests from a file. """ """ base class for collecting tests from a file. """
class Item(Node): class Item(Node):
""" a basic test invocation item. Note that for a single function """ a basic test invocation item. Note that for a single function
there might be multiple test invocation items. there might be multiple test invocation items.
@ -503,6 +551,21 @@ def __init__(self, name, parent=None, config=None, session=None):
self._report_sections = [] self._report_sections = []
def add_report_section(self, when, key, content): def add_report_section(self, when, key, content):
"""
Adds a new report section, similar to what's done internally to add stdout and
stderr captured output::
item.add_report_section("call", "stdout", "report section contents")
:param str when:
One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
:param str key:
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
``"stderr"`` internally.
:param str content:
The full contents as a string.
"""
if content: if content:
self._report_sections.append((when, key, content)) self._report_sections.append((when, key, content))
@ -526,12 +589,15 @@ def location(self):
self._location = location self._location = location
return location return location
class NoMatch(Exception): class NoMatch(Exception):
""" raised if matching cannot locate a matching names. """ """ raised if matching cannot locate a matching names. """
class Interrupted(KeyboardInterrupt): class Interrupted(KeyboardInterrupt):
""" signals an interrupted test run. """ """ signals an interrupted test run. """
__module__ = 'builtins' # for py3 __module__ = 'builtins' # for py3
class Session(FSCollector): class Session(FSCollector):
Interrupted = Interrupted Interrupted = Interrupted
@ -550,12 +616,12 @@ def __init__(self, config):
def _makeid(self): def _makeid(self):
return "" return ""
@pytest.hookimpl(tryfirst=True) @hookimpl(tryfirst=True)
def pytest_collectstart(self): def pytest_collectstart(self):
if self.shouldstop: if self.shouldstop:
raise self.Interrupted(self.shouldstop) raise self.Interrupted(self.shouldstop)
@pytest.hookimpl(tryfirst=True) @hookimpl(tryfirst=True)
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
if report.failed and not hasattr(report, 'wasxfail'): if report.failed and not hasattr(report, 'wasxfail'):
self.testsfailed += 1 self.testsfailed += 1
@ -586,8 +652,9 @@ def perform_collect(self, args=None, genitems=True):
hook = self.config.hook hook = self.config.hook
try: try:
items = self._perform_collect(args, genitems) items = self._perform_collect(args, genitems)
self.config.pluginmanager.check_pending()
hook.pytest_collection_modifyitems(session=self, hook.pytest_collection_modifyitems(session=self,
config=self.config, items=items) config=self.config, items=items)
finally: finally:
hook.pytest_collection_finish(session=self) hook.pytest_collection_finish(session=self)
self.testscollected = len(items) self.testscollected = len(items)
@ -614,8 +681,8 @@ def _perform_collect(self, args, genitems):
for arg, exc in self._notfound: for arg, exc in self._notfound:
line = "(no name %r in any of %r)" % (arg, exc.args[0]) line = "(no name %r in any of %r)" % (arg, exc.args[0])
errors.append("not found: %s\n%s" % (arg, line)) errors.append("not found: %s\n%s" % (arg, line))
#XXX: test this # XXX: test this
raise pytest.UsageError(*errors) raise UsageError(*errors)
if not genitems: if not genitems:
return rep.result return rep.result
else: else:
@ -643,7 +710,7 @@ def _collect(self, arg):
names = self._parsearg(arg) names = self._parsearg(arg)
path = names.pop(0) path = names.pop(0)
if path.check(dir=1): if path.check(dir=1):
assert not names, "invalid arg %r" %(arg,) assert not names, "invalid arg %r" % (arg,)
for path in path.visit(fil=lambda x: x.check(file=1), for path in path.visit(fil=lambda x: x.check(file=1),
rec=self._recurse, bf=True, sort=True): rec=self._recurse, bf=True, sort=True):
for x in self._collectfile(path): for x in self._collectfile(path):
@ -702,9 +769,11 @@ def _parsearg(self, arg):
path = self.config.invocation_dir.join(relpath, abs=True) path = self.config.invocation_dir.join(relpath, abs=True)
if not path.check(): if not path.check():
if self.config.option.pyargs: if self.config.option.pyargs:
raise pytest.UsageError("file or package not found: " + arg + " (missing __init__.py?)") raise UsageError(
"file or package not found: " + arg +
" (missing __init__.py?)")
else: else:
raise pytest.UsageError("file not found: " + arg) raise UsageError("file not found: " + arg)
parts[0] = path parts[0] = path
return parts return parts
@ -727,11 +796,11 @@ def _matchnodes(self, matching, names):
nextnames = names[1:] nextnames = names[1:]
resultnodes = [] resultnodes = []
for node in matching: for node in matching:
if isinstance(node, pytest.Item): if isinstance(node, Item):
if not names: if not names:
resultnodes.append(node) resultnodes.append(node)
continue continue
assert isinstance(node, pytest.Collector) assert isinstance(node, Collector)
rep = collect_one_node(node) rep = collect_one_node(node)
if rep.passed: if rep.passed:
has_matched = False has_matched = False
@ -744,16 +813,20 @@ def _matchnodes(self, matching, names):
if not has_matched and len(rep.result) == 1 and x.name == "()": if not has_matched and len(rep.result) == 1 and x.name == "()":
nextnames.insert(0, name) nextnames.insert(0, name)
resultnodes.extend(self.matchnodes([x], nextnames)) resultnodes.extend(self.matchnodes([x], nextnames))
node.ihook.pytest_collectreport(report=rep) else:
# report collection failures here to avoid failing to run some test
# specified in the command line because the module could not be
# imported (#134)
node.ihook.pytest_collectreport(report=rep)
return resultnodes return resultnodes
def genitems(self, node): def genitems(self, node):
self.trace("genitems", node) self.trace("genitems", node)
if isinstance(node, pytest.Item): if isinstance(node, Item):
node.ihook.pytest_itemcollected(item=node) node.ihook.pytest_itemcollected(item=node)
yield node yield node
else: else:
assert isinstance(node, pytest.Collector) assert isinstance(node, Collector)
rep = collect_one_node(node) rep = collect_one_node(node)
if rep.passed: if rep.passed:
for subnode in rep.result: for subnode in rep.result:

View File

@ -1,5 +1,75 @@
""" generic mechanism for marking and selecting python functions. """ """ generic mechanism for marking and selecting python functions. """
from __future__ import absolute_import, division, print_function
import inspect import inspect
import warnings
from collections import namedtuple
from operator import attrgetter
from .compat import imap
from .deprecated import MARK_PARAMETERSET_UNPACKING
def alias(name, warning=None):
getter = attrgetter(name)
def warned(self):
warnings.warn(warning, stacklevel=2)
return getter(self)
return property(getter if warning is None else warned, doc='alias for ' + name)
class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
@classmethod
def param(cls, *values, **kw):
marks = kw.pop('marks', ())
if isinstance(marks, MarkDecorator):
marks = marks,
else:
assert isinstance(marks, (tuple, list, set))
def param_extract_id(id=None):
return id
id = param_extract_id(**kw)
return cls(values, marks, id)
@classmethod
def extract_from(cls, parameterset, legacy_force_tuple=False):
"""
:param parameterset:
a legacy style parameterset that may or may not be a tuple,
and may or may not be wrapped into a mess of mark objects
:param legacy_force_tuple:
enforce tuple wrapping so single argument tuple values
don't get decomposed and break tests
"""
if isinstance(parameterset, cls):
return parameterset
if not isinstance(parameterset, MarkDecorator) and legacy_force_tuple:
return cls.param(parameterset)
newmarks = []
argval = parameterset
while isinstance(argval, MarkDecorator):
newmarks.append(MarkDecorator(Mark(
argval.markname, argval.args[:-1], argval.kwargs)))
argval = argval.args[-1]
assert not isinstance(argval, ParameterSet)
if legacy_force_tuple:
argval = argval,
if newmarks:
warnings.warn(MARK_PARAMETERSET_UNPACKING)
return cls(argval, marks=newmarks, id=None)
@property
def deprecated_arg_dict(self):
return dict((mark.name, mark) for mark in self.marks)
class MarkerError(Exception): class MarkerError(Exception):
@ -7,8 +77,8 @@ class MarkerError(Exception):
"""Error in use of a pytest marker/attribute.""" """Error in use of a pytest marker/attribute."""
def pytest_namespace(): def param(*values, **kw):
return {'mark': MarkGenerator()} return ParameterSet.param(*values, **kw)
def pytest_addoption(parser): def pytest_addoption(parser):
@ -21,7 +91,8 @@ def pytest_addoption(parser):
"where all names are substring-matched against test names " "where all names are substring-matched against test names "
"and their parent classes. Example: -k 'test_method or test_" "and their parent classes. Example: -k 'test_method or test_"
"other' matches all test functions and classes whose name " "other' matches all test functions and classes whose name "
"contains 'test_method' or 'test_other'. " "contains 'test_method' or 'test_other', while -k 'not test_method' "
"matches those that don't contain 'test_method' in their names. "
"Additionally keywords are matched to classes and functions " "Additionally keywords are matched to classes and functions "
"containing extra names in their 'extra_keyword_matches' set, " "containing extra names in their 'extra_keyword_matches' set, "
"as well as functions which have names assigned directly to them." "as well as functions which have names assigned directly to them."
@ -66,7 +137,7 @@ def pytest_collection_modifyitems(items, config):
return return
# pytest used to allow "-" for negating # pytest used to allow "-" for negating
# but today we just allow "-" at the beginning, use "not" instead # but today we just allow "-" at the beginning, use "not" instead
# we probably remove "-" alltogether soon # we probably remove "-" altogether soon
if keywordexpr.startswith("-"): if keywordexpr.startswith("-"):
keywordexpr = "not " + keywordexpr[1:] keywordexpr = "not " + keywordexpr[1:]
selectuntil = False selectuntil = False
@ -96,6 +167,7 @@ def pytest_collection_modifyitems(items, config):
class MarkMapping: class MarkMapping:
"""Provides a local mapping for markers where item access """Provides a local mapping for markers where item access
resolves to True if the marker is present. """ resolves to True if the marker is present. """
def __init__(self, keywords): def __init__(self, keywords):
mymarks = set() mymarks = set()
for key, value in keywords.items(): for key, value in keywords.items():
@ -111,6 +183,7 @@ class KeywordMapping:
"""Provides a local mapping for keywords. """Provides a local mapping for keywords.
Given a list of names, map any substring of one of these names to True. Given a list of names, map any substring of one of these names to True.
""" """
def __init__(self, names): def __init__(self, names):
self._names = names self._names = names
@ -162,9 +235,13 @@ def matchkeyword(colitem, keywordexpr):
def pytest_configure(config): def pytest_configure(config):
import pytest config._old_mark_config = MARK_GEN._config
if config.option.strict: if config.option.strict:
pytest.mark._config = config MARK_GEN._config = config
def pytest_unconfigure(config):
MARK_GEN._config = getattr(config, '_old_mark_config', None)
class MarkGenerator: class MarkGenerator:
@ -178,13 +255,14 @@ def test_function():
will set a 'slowtest' :class:`MarkInfo` object will set a 'slowtest' :class:`MarkInfo` object
on the ``test_function`` object. """ on the ``test_function`` object. """
_config = None
def __getattr__(self, name): def __getattr__(self, name):
if name[0] == "_": if name[0] == "_":
raise AttributeError("Marker name must NOT start with underscore") raise AttributeError("Marker name must NOT start with underscore")
if hasattr(self, '_config'): if self._config is not None:
self._check(name) self._check(name)
return MarkDecorator(name) return MarkDecorator(Mark(name, (), {}))
def _check(self, name): def _check(self, name):
try: try:
@ -192,18 +270,21 @@ def _check(self, name):
return return
except AttributeError: except AttributeError:
pass pass
self._markers = l = set() self._markers = values = set()
for line in self._config.getini("markers"): for line in self._config.getini("markers"):
beginning = line.split(":", 1) marker, _ = line.split(":", 1)
x = beginning[0].split("(", 1)[0] marker = marker.rstrip()
l.add(x) x = marker.split("(", 1)[0]
values.add(x)
if name not in self._markers: if name not in self._markers:
raise AttributeError("%r not a registered marker" % (name,)) raise AttributeError("%r not a registered marker" % (name,))
def istestfunc(func): def istestfunc(func):
return hasattr(func, "__call__") and \ return hasattr(func, "__call__") and \
getattr(func, "__name__", "<lambda>") != "<lambda>" getattr(func, "__name__", "<lambda>") != "<lambda>"
class MarkDecorator: class MarkDecorator:
""" A decorator for test functions and test classes. When applied """ A decorator for test functions and test classes. When applied
it will create :class:`MarkInfo` objects which may be it will create :class:`MarkInfo` objects which may be
@ -237,19 +318,35 @@ def test_function():
additional keyword or positional arguments. additional keyword or positional arguments.
""" """
def __init__(self, name, args=None, kwargs=None):
self.name = name def __init__(self, mark):
self.args = args or () assert isinstance(mark, Mark), repr(mark)
self.kwargs = kwargs or {} self.mark = mark
name = alias('mark.name')
args = alias('mark.args')
kwargs = alias('mark.kwargs')
@property @property
def markname(self): def markname(self):
return self.name # for backward-compat (2.4.1 had this attr) return self.name # for backward-compat (2.4.1 had this attr)
def __eq__(self, other):
return self.mark == other.mark if isinstance(other, MarkDecorator) else False
def __repr__(self): def __repr__(self):
d = self.__dict__.copy() return "<MarkDecorator %r>" % (self.mark,)
name = d.pop('name')
return "<MarkDecorator %r %r>" % (name, d) def with_args(self, *args, **kwargs):
""" return a MarkDecorator with extra arguments added
unlike call this can be used even if the sole argument is a callable/class
:return: MarkDecorator
"""
mark = Mark(self.name, args, kwargs)
return self.__class__(self.mark.combined_with(mark))
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
""" if passed a single callable argument: decorate it with mark info. """ if passed a single callable argument: decorate it with mark info.
@ -259,70 +356,110 @@ def __call__(self, *args, **kwargs):
is_class = inspect.isclass(func) is_class = inspect.isclass(func)
if len(args) == 1 and (istestfunc(func) or is_class): if len(args) == 1 and (istestfunc(func) or is_class):
if is_class: if is_class:
if hasattr(func, 'pytestmark'): store_mark(func, self.mark)
mark_list = func.pytestmark
if not isinstance(mark_list, list):
mark_list = [mark_list]
# always work on a copy to avoid updating pytestmark
# from a superclass by accident
mark_list = mark_list + [self]
func.pytestmark = mark_list
else:
func.pytestmark = [self]
else: else:
holder = getattr(func, self.name, None) store_legacy_markinfo(func, self.mark)
if holder is None: store_mark(func, self.mark)
holder = MarkInfo(
self.name, self.args, self.kwargs
)
setattr(func, self.name, holder)
else:
holder.add(self.args, self.kwargs)
return func return func
kw = self.kwargs.copy() return self.with_args(*args, **kwargs)
kw.update(kwargs)
args = self.args + args
return self.__class__(self.name, args=args, kwargs=kw)
def extract_argvalue(maybe_marked_args): def get_unpacked_marks(obj):
# TODO: incorrect mark data, the old code wanst able to collect lists """
# individual parametrized argument sets can be wrapped in a series obtain the unpacked marks that are stored on a object
# of markers in which case we unwrap the values and apply the mark """
# at Function init mark_list = getattr(obj, 'pytestmark', [])
newmarks = {}
argval = maybe_marked_args if not isinstance(mark_list, list):
while isinstance(argval, MarkDecorator): mark_list = [mark_list]
newmark = MarkDecorator(argval.markname, return [
argval.args[:-1], argval.kwargs) getattr(mark, 'mark', mark) # unpack MarkDecorator
newmarks[newmark.markname] = newmark for mark in mark_list
argval = argval.args[-1] ]
return argval, newmarks
class MarkInfo: def store_mark(obj, mark):
"""store a Mark on a object
this is used to implement the Mark declarations/decorators correctly
"""
assert isinstance(mark, Mark), mark
# always reassign name to avoid updating pytestmark
# in a reference that was only borrowed
obj.pytestmark = get_unpacked_marks(obj) + [mark]
def store_legacy_markinfo(func, mark):
"""create the legacy MarkInfo objects and put them onto the function
"""
if not isinstance(mark, Mark):
raise TypeError("got {mark!r} instead of a Mark".format(mark=mark))
holder = getattr(func, mark.name, None)
if holder is None:
holder = MarkInfo(mark)
setattr(func, mark.name, holder)
else:
holder.add_mark(mark)
class Mark(namedtuple('Mark', 'name, args, kwargs')):
def combined_with(self, other):
assert self.name == other.name
return Mark(
self.name, self.args + other.args,
dict(self.kwargs, **other.kwargs))
class MarkInfo(object):
""" Marking object created by :class:`MarkDecorator` instances. """ """ Marking object created by :class:`MarkDecorator` instances. """
def __init__(self, name, args, kwargs):
#: name of attribute def __init__(self, mark):
self.name = name assert isinstance(mark, Mark), repr(mark)
#: positional argument list, empty if none specified self.combined = mark
self.args = args self._marks = [mark]
#: keyword argument dictionary, empty if nothing specified
self.kwargs = kwargs.copy() name = alias('combined.name')
self._arglist = [(args, kwargs.copy())] args = alias('combined.args')
kwargs = alias('combined.kwargs')
def __repr__(self): def __repr__(self):
return "<MarkInfo %r args=%r kwargs=%r>" % ( return "<MarkInfo {0!r}>".format(self.combined)
self.name, self.args, self.kwargs
)
def add(self, args, kwargs): def add_mark(self, mark):
""" add a MarkInfo with the given args and kwargs. """ """ add a MarkInfo with the given args and kwargs. """
self._arglist.append((args, kwargs)) self._marks.append(mark)
self.args += args self.combined = self.combined.combined_with(mark)
self.kwargs.update(kwargs)
def __iter__(self): def __iter__(self):
""" yield MarkInfo objects each relating to a marking-call. """ """ yield MarkInfo objects each relating to a marking-call. """
for args, kwargs in self._arglist: return imap(MarkInfo, self._marks)
yield MarkInfo(self.name, args, kwargs)
MARK_GEN = MarkGenerator()
def _marked(func, mark):
""" Returns True if :func: is already marked with :mark:, False otherwise.
This can happen if marker is applied to class and the test file is
invoked more than once.
"""
try:
func_mark = getattr(func, mark.name)
except AttributeError:
return False
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
def transfer_markers(funcobj, cls, mod):
"""
this function transfers class level markers and module level markers
into function level markinfo objects
this is the main reason why marks are so broken
the resolution will involve phasing out function level MarkInfo objects
"""
for obj in (cls, mod):
for mark in get_unpacked_marks(obj):
if not _marked(funcobj, mark):
store_legacy_markinfo(funcobj, mark)

View File

@ -1,17 +1,18 @@
""" monkeypatching and mocking functionality. """ """ monkeypatching and mocking functionality. """
from __future__ import absolute_import, division, print_function
import os, sys import os
import sys
import re import re
from py.builtin import _basestring from py.builtin import _basestring
from _pytest.fixtures import fixture
import pytest
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
@pytest.fixture @fixture
def monkeypatch(request): def monkeypatch():
"""The returned ``monkeypatch`` fixture provides these """The returned ``monkeypatch`` fixture provides these
helper methods to modify objects, dictionaries or os.environ:: helper methods to modify objects, dictionaries or os.environ::
@ -30,8 +31,8 @@ def monkeypatch(request):
will be raised if the set/deletion operation has no target. will be raised if the set/deletion operation has no target.
""" """
mpatch = MonkeyPatch() mpatch = MonkeyPatch()
request.addfinalizer(mpatch.undo) yield mpatch
return mpatch mpatch.undo()
def resolve(name): def resolve(name):
@ -70,9 +71,9 @@ def annotated_getattr(obj, name, ann):
obj = getattr(obj, name) obj = getattr(obj, name)
except AttributeError: except AttributeError:
raise AttributeError( raise AttributeError(
'%r object at %s has no attribute %r' % ( '%r object at %s has no attribute %r' % (
type(obj).__name__, ann, name type(obj).__name__, ann, name
) )
) )
return obj return obj

37
lib/spack/external/_pytest/nodes.py vendored Normal file
View File

@ -0,0 +1,37 @@
SEP = "/"
def _splitnode(nodeid):
"""Split a nodeid into constituent 'parts'.
Node IDs are strings, and can be things like:
''
'testing/code'
'testing/code/test_excinfo.py'
'testing/code/test_excinfo.py::TestFormattedExcinfo::()'
Return values are lists e.g.
[]
['testing', 'code']
['testing', 'code', 'test_excinfo.py']
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()']
"""
if nodeid == '':
# If there is no root node at all, return an empty list so the caller's logic can remain sane
return []
parts = nodeid.split(SEP)
# Replace single last element 'test_foo.py::Bar::()' with multiple elements 'test_foo.py', 'Bar', '()'
parts[-1:] = parts[-1].split("::")
return parts
def ischildnode(baseid, nodeid):
"""Return True if the nodeid is a child node of the baseid.
E.g. 'foo/bar::Baz::()' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp'
"""
base_parts = _splitnode(baseid)
node_parts = _splitnode(nodeid)
if len(node_parts) < len(base_parts):
return False
return node_parts[:len(base_parts)] == base_parts

View File

@ -1,10 +1,11 @@
""" run test suites written for nose. """ """ run test suites written for nose. """
from __future__ import absolute_import, division, print_function
import sys import sys
import py import py
import pytest from _pytest import unittest, runner, python
from _pytest import unittest from _pytest.config import hookimpl
def get_skip_exceptions(): def get_skip_exceptions():
@ -19,45 +20,46 @@ def get_skip_exceptions():
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()): if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()):
# let's substitute the excinfo with a pytest.skip one # let's substitute the excinfo with a pytest.skip one
call2 = call.__class__(lambda: call2 = call.__class__(
pytest.skip(str(call.excinfo.value)), call.when) lambda: runner.skip(str(call.excinfo.value)), call.when)
call.excinfo = call2.excinfo call.excinfo = call2.excinfo
@pytest.hookimpl(trylast=True) @hookimpl(trylast=True)
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
if is_potential_nosetest(item): if is_potential_nosetest(item):
if isinstance(item.parent, pytest.Generator): if isinstance(item.parent, python.Generator):
gen = item.parent gen = item.parent
if not hasattr(gen, '_nosegensetup'): if not hasattr(gen, '_nosegensetup'):
call_optional(gen.obj, 'setup') call_optional(gen.obj, 'setup')
if isinstance(gen.parent, pytest.Instance): if isinstance(gen.parent, python.Instance):
call_optional(gen.parent.obj, 'setup') call_optional(gen.parent.obj, 'setup')
gen._nosegensetup = True gen._nosegensetup = True
if not call_optional(item.obj, 'setup'): if not call_optional(item.obj, 'setup'):
# call module level setup if there is no object level one # call module level setup if there is no object level one
call_optional(item.parent.obj, 'setup') call_optional(item.parent.obj, 'setup')
#XXX this implies we only call teardown when setup worked # XXX this implies we only call teardown when setup worked
item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item) item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item)
def teardown_nose(item): def teardown_nose(item):
if is_potential_nosetest(item): if is_potential_nosetest(item):
if not call_optional(item.obj, 'teardown'): if not call_optional(item.obj, 'teardown'):
call_optional(item.parent.obj, 'teardown') call_optional(item.parent.obj, 'teardown')
#if hasattr(item.parent, '_nosegensetup'): # if hasattr(item.parent, '_nosegensetup'):
# #call_optional(item._nosegensetup, 'teardown') # #call_optional(item._nosegensetup, 'teardown')
# del item.parent._nosegensetup # del item.parent._nosegensetup
def pytest_make_collect_report(collector): def pytest_make_collect_report(collector):
if isinstance(collector, pytest.Generator): if isinstance(collector, python.Generator):
call_optional(collector.obj, 'setup') call_optional(collector.obj, 'setup')
def is_potential_nosetest(item): def is_potential_nosetest(item):
# extra check needed since we do not do nose style setup/teardown # extra check needed since we do not do nose style setup/teardown
# on direct unittest style classes # on direct unittest style classes
return isinstance(item, pytest.Function) and \ return isinstance(item, python.Function) and \
not isinstance(item, unittest.TestCaseFunction) not isinstance(item, unittest.TestCaseFunction)

140
lib/spack/external/_pytest/outcomes.py vendored Normal file
View File

@ -0,0 +1,140 @@
"""
exception classes and constants handling test outcomes
as well as functions creating them
"""
from __future__ import absolute_import, division, print_function
import py
import sys
class OutcomeException(BaseException):
""" OutcomeException and its subclass instances indicate and
contain info about test and collection outcomes.
"""
def __init__(self, msg=None, pytrace=True):
BaseException.__init__(self, msg)
self.msg = msg
self.pytrace = pytrace
def __repr__(self):
if self.msg:
val = self.msg
if isinstance(val, bytes):
val = py._builtin._totext(val, errors='replace')
return val
return "<%s instance>" % (self.__class__.__name__,)
__str__ = __repr__
TEST_OUTCOME = (OutcomeException, Exception)
class Skipped(OutcomeException):
# XXX hackish: on 3k we fake to live in the builtins
# in order to have Skipped exception printing shorter/nicer
__module__ = 'builtins'
def __init__(self, msg=None, pytrace=True, allow_module_level=False):
OutcomeException.__init__(self, msg=msg, pytrace=pytrace)
self.allow_module_level = allow_module_level
class Failed(OutcomeException):
""" raised from an explicit call to pytest.fail() """
__module__ = 'builtins'
class Exit(KeyboardInterrupt):
""" raised for immediate program exits (no tracebacks/summaries)"""
def __init__(self, msg="unknown reason"):
self.msg = msg
KeyboardInterrupt.__init__(self, msg)
# exposed helper methods
def exit(msg):
""" exit testing process as if KeyboardInterrupt was triggered. """
__tracebackhide__ = True
raise Exit(msg)
exit.Exception = Exit
def skip(msg=""):
""" skip an executing test with the given message. Note: it's usually
better to use the pytest.mark.skipif marker to declare a test to be
skipped under certain conditions like mismatching platforms or
dependencies. See the pytest_skipping plugin for details.
"""
__tracebackhide__ = True
raise Skipped(msg=msg)
skip.Exception = Skipped
def fail(msg="", pytrace=True):
""" explicitly fail an currently-executing test with the given Message.
:arg pytrace: if false the msg represents the full failure information
and no python traceback will be reported.
"""
__tracebackhide__ = True
raise Failed(msg=msg, pytrace=pytrace)
fail.Exception = Failed
class XFailed(fail.Exception):
""" raised from an explicit call to pytest.xfail() """
def xfail(reason=""):
""" xfail an executing test or setup functions with the given reason."""
__tracebackhide__ = True
raise XFailed(reason)
xfail.Exception = XFailed
def importorskip(modname, minversion=None):
""" return imported module if it has at least "minversion" as its
__version__ attribute. If no minversion is specified the a skip
is only triggered if the module can not be imported.
"""
import warnings
__tracebackhide__ = True
compile(modname, '', 'eval') # to catch syntaxerrors
should_skip = False
with warnings.catch_warnings():
# make sure to ignore ImportWarnings that might happen because
# of existing directories with the same name we're trying to
# import but without a __init__.py file
warnings.simplefilter('ignore')
try:
__import__(modname)
except ImportError:
# Do not raise chained exception here(#1485)
should_skip = True
if should_skip:
raise Skipped("could not import %r" % (modname,), allow_module_level=True)
mod = sys.modules[modname]
if minversion is None:
return mod
verattr = getattr(mod, '__version__', None)
if minversion is not None:
try:
from pkg_resources import parse_version as pv
except ImportError:
raise Skipped("we have a required version for %r but can not import "
"pkg_resources to parse version strings." % (modname,),
allow_module_level=True)
if verattr is None or pv(verattr) < pv(minversion):
raise Skipped("module %r has __version__ %r, required is: %r" % (
modname, verattr, minversion), allow_module_level=True)
return mod

View File

@ -1,4 +1,6 @@
""" submit failure or test session information to a pastebin service. """ """ submit failure or test session information to a pastebin service. """
from __future__ import absolute_import, division, print_function
import pytest import pytest
import sys import sys
import tempfile import tempfile
@ -7,9 +9,9 @@
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("terminal reporting") group = parser.getgroup("terminal reporting")
group._addoption('--pastebin', metavar="mode", group._addoption('--pastebin', metavar="mode",
action='store', dest="pastebin", default=None, action='store', dest="pastebin", default=None,
choices=['failed', 'all'], choices=['failed', 'all'],
help="send failed|all info to bpaste.net pastebin service.") help="send failed|all info to bpaste.net pastebin service.")
@pytest.hookimpl(trylast=True) @pytest.hookimpl(trylast=True)
@ -95,4 +97,4 @@ def pytest_terminal_summary(terminalreporter):
s = tw.stringio.getvalue() s = tw.stringio.getvalue()
assert len(s) assert len(s)
pastebinurl = create_new_paste(s) pastebinurl = create_new_paste(s)
tr.write_line("%s --> %s" %(msg, pastebinurl)) tr.write_line("%s --> %s" % (msg, pastebinurl))

View File

@ -1,4 +1,6 @@
""" (disabled by default) support for testing pytest and pytest plugins. """ """ (disabled by default) support for testing pytest and pytest plugins. """
from __future__ import absolute_import, division, print_function
import codecs import codecs
import gc import gc
import os import os
@ -10,8 +12,9 @@
import traceback import traceback
from fnmatch import fnmatch from fnmatch import fnmatch
from py.builtin import print_ from weakref import WeakKeyDictionary
from _pytest.capture import MultiCapture, SysCapture
from _pytest._code import Source from _pytest._code import Source
import py import py
import pytest import pytest
@ -22,13 +25,13 @@
def pytest_addoption(parser): def pytest_addoption(parser):
# group = parser.getgroup("pytester", "pytester (self-tests) options") # group = parser.getgroup("pytester", "pytester (self-tests) options")
parser.addoption('--lsof', parser.addoption('--lsof',
action="store_true", dest="lsof", default=False, action="store_true", dest="lsof", default=False,
help=("run FD checks if lsof is available")) help=("run FD checks if lsof is available"))
parser.addoption('--runpytest', default="inprocess", dest="runpytest", parser.addoption('--runpytest', default="inprocess", dest="runpytest",
choices=("inprocess", "subprocess", ), choices=("inprocess", "subprocess", ),
help=("run pytest sub runs in tests using an 'inprocess' " help=("run pytest sub runs in tests using an 'inprocess' "
"or 'subprocess' (python -m main) method")) "or 'subprocess' (python -m main) method"))
def pytest_configure(config): def pytest_configure(config):
@ -59,7 +62,7 @@ def _exec_lsof(self):
def _parse_lsof_output(self, out): def _parse_lsof_output(self, out):
def isopen(line): def isopen(line):
return line.startswith('f') and ("deleted" not in line and return line.startswith('f') and ("deleted" not in line and
'mem' not in line and "txt" not in line and 'cwd' not in line) 'mem' not in line and "txt" not in line and 'cwd' not in line)
open_files = [] open_files = []
@ -85,7 +88,7 @@ def matching_platform(self):
return True return True
@pytest.hookimpl(hookwrapper=True, tryfirst=True) @pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_item(self, item): def pytest_runtest_protocol(self, item):
lines1 = self.get_open_files() lines1 = self.get_open_files()
yield yield
if hasattr(sys, "pypy_version_info"): if hasattr(sys, "pypy_version_info"):
@ -104,7 +107,8 @@ def pytest_runtest_item(self, item):
error.extend([str(f) for f in lines2]) error.extend([str(f) for f in lines2])
error.append(error[0]) error.append(error[0])
error.append("*** function %s:%s: %s " % item.location) error.append("*** function %s:%s: %s " % item.location)
pytest.fail("\n".join(error), pytrace=False) error.append("See issue #2366")
item.warn('', "\n".join(error))
# XXX copied from execnet's conftest.py - needs to be merged # XXX copied from execnet's conftest.py - needs to be merged
@ -118,6 +122,7 @@ def pytest_runtest_item(self, item):
'python3.5': r'C:\Python35\python.exe', 'python3.5': r'C:\Python35\python.exe',
} }
def getexecutable(name, cache={}): def getexecutable(name, cache={}):
try: try:
return cache[name] return cache[name]
@ -126,19 +131,20 @@ def getexecutable(name, cache={}):
if executable: if executable:
import subprocess import subprocess
popen = subprocess.Popen([str(executable), "--version"], popen = subprocess.Popen([str(executable), "--version"],
universal_newlines=True, stderr=subprocess.PIPE) universal_newlines=True, stderr=subprocess.PIPE)
out, err = popen.communicate() out, err = popen.communicate()
if name == "jython": if name == "jython":
if not err or "2.5" not in err: if not err or "2.5" not in err:
executable = None executable = None
if "2.5.2" in err: if "2.5.2" in err:
executable = None # http://bugs.jython.org/issue1790 executable = None # http://bugs.jython.org/issue1790
elif popen.returncode != 0: elif popen.returncode != 0:
# Handle pyenv's 127. # Handle pyenv's 127.
executable = None executable = None
cache[name] = executable cache[name] = executable
return executable return executable
@pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", @pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4",
'pypy', 'pypy3']) 'pypy', 'pypy3'])
def anypython(request): def anypython(request):
@ -155,6 +161,8 @@ def anypython(request):
return executable return executable
# used at least by pytest-xdist plugin # used at least by pytest-xdist plugin
@pytest.fixture @pytest.fixture
def _pytest(request): def _pytest(request):
""" Return a helper which offers a gethookrecorder(hook) """ Return a helper which offers a gethookrecorder(hook)
@ -163,6 +171,7 @@ def _pytest(request):
""" """
return PytestArg(request) return PytestArg(request)
class PytestArg: class PytestArg:
def __init__(self, request): def __init__(self, request):
self.request = request self.request = request
@ -173,9 +182,9 @@ def gethookrecorder(self, hook):
return hookrecorder return hookrecorder
def get_public_names(l): def get_public_names(values):
"""Only return names from iterator l without a leading underscore.""" """Only return names from iterator values without a leading underscore."""
return [x for x in l if x[0] != "_"] return [x for x in values if x[0] != "_"]
class ParsedCall: class ParsedCall:
@ -186,7 +195,7 @@ def __init__(self, name, kwargs):
def __repr__(self): def __repr__(self):
d = self.__dict__.copy() d = self.__dict__.copy()
del d['_name'] del d['_name']
return "<ParsedCall %r(**%r)>" %(self._name, d) return "<ParsedCall %r(**%r)>" % (self._name, d)
class HookRecorder: class HookRecorder:
@ -226,15 +235,15 @@ def assert_contains(self, entries):
name, check = entries.pop(0) name, check = entries.pop(0)
for ind, call in enumerate(self.calls[i:]): for ind, call in enumerate(self.calls[i:]):
if call._name == name: if call._name == name:
print_("NAMEMATCH", name, call) print("NAMEMATCH", name, call)
if eval(check, backlocals, call.__dict__): if eval(check, backlocals, call.__dict__):
print_("CHECKERMATCH", repr(check), "->", call) print("CHECKERMATCH", repr(check), "->", call)
else: else:
print_("NOCHECKERMATCH", repr(check), "-", call) print("NOCHECKERMATCH", repr(check), "-", call)
continue continue
i += ind + 1 i += ind + 1
break break
print_("NONAMEMATCH", name, "with", call) print("NONAMEMATCH", name, "with", call)
else: else:
pytest.fail("could not find %r check %r" % (name, check)) pytest.fail("could not find %r check %r" % (name, check))
@ -249,9 +258,9 @@ def popcall(self, name):
pytest.fail("\n".join(lines)) pytest.fail("\n".join(lines))
def getcall(self, name): def getcall(self, name):
l = self.getcalls(name) values = self.getcalls(name)
assert len(l) == 1, (name, l) assert len(values) == 1, (name, values)
return l[0] return values[0]
# functionality for test reports # functionality for test reports
@ -260,9 +269,9 @@ def getreports(self,
return [x.report for x in self.getcalls(names)] return [x.report for x in self.getcalls(names)]
def matchreport(self, inamepart="", def matchreport(self, inamepart="",
names="pytest_runtest_logreport pytest_collectreport", when=None): names="pytest_runtest_logreport pytest_collectreport", when=None):
""" return a testreport whose dotted import path matches """ """ return a testreport whose dotted import path matches """
l = [] values = []
for rep in self.getreports(names=names): for rep in self.getreports(names=names):
try: try:
if not when and rep.when != "call" and rep.passed: if not when and rep.when != "call" and rep.passed:
@ -273,14 +282,14 @@ def matchreport(self, inamepart="",
if when and getattr(rep, 'when', None) != when: if when and getattr(rep, 'when', None) != when:
continue continue
if not inamepart or inamepart in rep.nodeid.split("::"): if not inamepart or inamepart in rep.nodeid.split("::"):
l.append(rep) values.append(rep)
if not l: if not values:
raise ValueError("could not find test report matching %r: " raise ValueError("could not find test report matching %r: "
"no test reports at all!" % (inamepart,)) "no test reports at all!" % (inamepart,))
if len(l) > 1: if len(values) > 1:
raise ValueError( raise ValueError(
"found 2 or more testreports matching %r: %s" %(inamepart, l)) "found 2 or more testreports matching %r: %s" % (inamepart, values))
return l[0] return values[0]
def getfailures(self, def getfailures(self,
names='pytest_runtest_logreport pytest_collectreport'): names='pytest_runtest_logreport pytest_collectreport'):
@ -294,7 +303,7 @@ def listoutcomes(self):
skipped = [] skipped = []
failed = [] failed = []
for rep in self.getreports( for rep in self.getreports(
"pytest_collectreport pytest_runtest_logreport"): "pytest_collectreport pytest_runtest_logreport"):
if rep.passed: if rep.passed:
if getattr(rep, "when", None) == "call": if getattr(rep, "when", None) == "call":
passed.append(rep) passed.append(rep)
@ -332,7 +341,9 @@ def testdir(request, tmpdir_factory):
return Testdir(request, tmpdir_factory) return Testdir(request, tmpdir_factory)
rex_outcome = re.compile("(\d+) ([\w-]+)") rex_outcome = re.compile(r"(\d+) ([\w-]+)")
class RunResult: class RunResult:
"""The result of running a command. """The result of running a command.
@ -348,6 +359,7 @@ class RunResult:
:duration: Duration in seconds. :duration: Duration in seconds.
""" """
def __init__(self, ret, outlines, errlines, duration): def __init__(self, ret, outlines, errlines, duration):
self.ret = ret self.ret = ret
self.outlines = outlines self.outlines = outlines
@ -367,15 +379,19 @@ def parseoutcomes(self):
for num, cat in outcomes: for num, cat in outcomes:
d[cat] = int(num) d[cat] = int(num)
return d return d
raise ValueError("Pytest terminal report not found")
def assert_outcomes(self, passed=0, skipped=0, failed=0): def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0):
""" assert that the specified outcomes appear with the respective """ assert that the specified outcomes appear with the respective
numbers (0 means it didn't occur) in the text output from a test run.""" numbers (0 means it didn't occur) in the text output from a test run."""
d = self.parseoutcomes() d = self.parseoutcomes()
assert passed == d.get("passed", 0) obtained = {
assert skipped == d.get("skipped", 0) 'passed': d.get('passed', 0),
assert failed == d.get("failed", 0) 'skipped': d.get('skipped', 0),
'failed': d.get('failed', 0),
'error': d.get('error', 0),
}
assert obtained == dict(passed=passed, skipped=skipped, failed=failed, error=error)
class Testdir: class Testdir:
@ -401,6 +417,7 @@ class Testdir:
def __init__(self, request, tmpdir_factory): def __init__(self, request, tmpdir_factory):
self.request = request self.request = request
self._mod_collections = WeakKeyDictionary()
# XXX remove duplication with tmpdir plugin # XXX remove duplication with tmpdir plugin
basetmp = tmpdir_factory.ensuretemp("testdir") basetmp = tmpdir_factory.ensuretemp("testdir")
name = request.function.__name__ name = request.function.__name__
@ -414,7 +431,7 @@ def __init__(self, request, tmpdir_factory):
self.plugins = [] self.plugins = []
self._savesyspath = (list(sys.path), list(sys.meta_path)) self._savesyspath = (list(sys.path), list(sys.meta_path))
self._savemodulekeys = set(sys.modules) self._savemodulekeys = set(sys.modules)
self.chdir() # always chdir self.chdir() # always chdir
self.request.addfinalizer(self.finalize) self.request.addfinalizer(self.finalize)
method = self.request.config.getoption("--runpytest") method = self.request.config.getoption("--runpytest")
if method == "inprocess": if method == "inprocess":
@ -446,9 +463,10 @@ def delete_loaded_modules(self):
the module is re-imported. the module is re-imported.
""" """
for name in set(sys.modules).difference(self._savemodulekeys): for name in set(sys.modules).difference(self._savemodulekeys):
# it seems zope.interfaces is keeping some state # some zope modules used by twisted-related tests keeps internal
# (used by twisted related tests) # state and can't be deleted; we had some trouble in the past
if name != "zope.interface": # with zope.interface for example
if not name.startswith("zope"):
del sys.modules[name] del sys.modules[name]
def make_hook_recorder(self, pluginmanager): def make_hook_recorder(self, pluginmanager):
@ -468,7 +486,7 @@ def chdir(self):
if not hasattr(self, '_olddir'): if not hasattr(self, '_olddir'):
self._olddir = old self._olddir = old
def _makefile(self, ext, args, kwargs): def _makefile(self, ext, args, kwargs, encoding="utf-8"):
items = list(kwargs.items()) items = list(kwargs.items())
if args: if args:
source = py.builtin._totext("\n").join( source = py.builtin._totext("\n").join(
@ -488,8 +506,8 @@ def my_totext(s, encoding="utf-8"):
source_unicode = "\n".join([my_totext(line) for line in source.lines]) source_unicode = "\n".join([my_totext(line) for line in source.lines])
source = py.builtin._totext(source_unicode) source = py.builtin._totext(source_unicode)
content = source.strip().encode("utf-8") # + "\n" content = source.strip().encode(encoding) # + "\n"
#content = content.rstrip() + "\n" # content = content.rstrip() + "\n"
p.write(content, "wb") p.write(content, "wb")
if ret is None: if ret is None:
ret = p ret = p
@ -565,7 +583,7 @@ def mkdir(self, name):
def mkpydir(self, name): def mkpydir(self, name):
"""Create a new python package. """Create a new python package.
This creates a (sub)direcotry with an empty ``__init__.py`` This creates a (sub)directory with an empty ``__init__.py``
file so that is recognised as a python package. file so that is recognised as a python package.
""" """
@ -574,6 +592,7 @@ def mkpydir(self, name):
return p return p
Session = Session Session = Session
def getnode(self, config, arg): def getnode(self, config, arg):
"""Return the collection node of a file. """Return the collection node of a file.
@ -654,13 +673,13 @@ def inline_runsource(self, source, *cmdlineargs):
""" """
p = self.makepyfile(source) p = self.makepyfile(source)
l = list(cmdlineargs) + [p] values = list(cmdlineargs) + [p]
return self.inline_run(*l) return self.inline_run(*values)
def inline_genitems(self, *args): def inline_genitems(self, *args):
"""Run ``pytest.main(['--collectonly'])`` in-process. """Run ``pytest.main(['--collectonly'])`` in-process.
Retuns a tuple of the collected items and a Returns a tuple of the collected items and a
:py:class:`HookRecorder` instance. :py:class:`HookRecorder` instance.
This runs the :py:func:`pytest.main` function to run all of This runs the :py:func:`pytest.main` function to run all of
@ -733,7 +752,8 @@ def runpytest_inprocess(self, *args, **kwargs):
if kwargs.get("syspathinsert"): if kwargs.get("syspathinsert"):
self.syspathinsert() self.syspathinsert()
now = time.time() now = time.time()
capture = py.io.StdCapture() capture = MultiCapture(Capture=SysCapture)
capture.start_capturing()
try: try:
try: try:
reprec = self.inline_run(*args, **kwargs) reprec = self.inline_run(*args, **kwargs)
@ -748,13 +768,14 @@ class reprec:
class reprec: class reprec:
ret = 3 ret = 3
finally: finally:
out, err = capture.reset() out, err = capture.readouterr()
capture.stop_capturing()
sys.stdout.write(out) sys.stdout.write(out)
sys.stderr.write(err) sys.stderr.write(err)
res = RunResult(reprec.ret, res = RunResult(reprec.ret,
out.split("\n"), err.split("\n"), out.split("\n"), err.split("\n"),
time.time()-now) time.time() - now)
res.reprec = reprec res.reprec = reprec
return res return res
@ -770,11 +791,11 @@ def _ensure_basetemp(self, args):
args = [str(x) for x in args] args = [str(x) for x in args]
for x in args: for x in args:
if str(x).startswith('--basetemp'): if str(x).startswith('--basetemp'):
#print ("basedtemp exists: %s" %(args,)) # print("basedtemp exists: %s" %(args,))
break break
else: else:
args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp')) args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp'))
#print ("added basetemp: %s" %(args,)) # print("added basetemp: %s" %(args,))
return args return args
def parseconfig(self, *args): def parseconfig(self, *args):
@ -812,7 +833,7 @@ def parseconfigure(self, *args):
self.request.addfinalizer(config._ensure_unconfigure) self.request.addfinalizer(config._ensure_unconfigure)
return config return config
def getitem(self, source, funcname="test_func"): def getitem(self, source, funcname="test_func"):
"""Return the test item for a test function. """Return the test item for a test function.
This writes the source to a python file and runs pytest's This writes the source to a python file and runs pytest's
@ -829,10 +850,10 @@ def getitem(self, source, funcname="test_func"):
for item in items: for item in items:
if item.name == funcname: if item.name == funcname:
return item return item
assert 0, "%r item not found in module:\n%s\nitems: %s" %( assert 0, "%r item not found in module:\n%s\nitems: %s" % (
funcname, source, items) funcname, source, items)
def getitems(self, source): def getitems(self, source):
"""Return all test items collected from the module. """Return all test items collected from the module.
This writes the source to a python file and runs pytest's This writes the source to a python file and runs pytest's
@ -843,7 +864,7 @@ def getitems(self, source):
modcol = self.getmodulecol(source) modcol = self.getmodulecol(source)
return self.genitems([modcol]) return self.genitems([modcol])
def getmodulecol(self, source, configargs=(), withinit=False): def getmodulecol(self, source, configargs=(), withinit=False):
"""Return the module collection node for ``source``. """Return the module collection node for ``source``.
This writes ``source`` to a file using :py:meth:`makepyfile` This writes ``source`` to a file using :py:meth:`makepyfile`
@ -856,15 +877,16 @@ def getmodulecol(self, source, configargs=(), withinit=False):
:py:meth:`parseconfigure`. :py:meth:`parseconfigure`.
:param withinit: Whether to also write a ``__init__.py`` file :param withinit: Whether to also write a ``__init__.py`` file
to the temporarly directory to ensure it is a package. to the temporary directory to ensure it is a package.
""" """
kw = {self.request.function.__name__: Source(source).strip()} kw = {self.request.function.__name__: Source(source).strip()}
path = self.makepyfile(**kw) path = self.makepyfile(**kw)
if withinit: if withinit:
self.makepyfile(__init__ = "#") self.makepyfile(__init__="#")
self.config = config = self.parseconfigure(path, *configargs) self.config = config = self.parseconfigure(path, *configargs)
node = self.getnode(config, path) node = self.getnode(config, path)
return node return node
def collect_by_name(self, modcol, name): def collect_by_name(self, modcol, name):
@ -879,7 +901,9 @@ def collect_by_name(self, modcol, name):
:param name: The name of the node to return. :param name: The name of the node to return.
""" """
for colitem in modcol._memocollect(): if modcol not in self._mod_collections:
self._mod_collections[modcol] = list(modcol.collect())
for colitem in self._mod_collections[modcol]:
if colitem.name == name: if colitem.name == name:
return colitem return colitem
@ -896,8 +920,11 @@ def popen(self, cmdargs, stdout, stderr, **kw):
env['PYTHONPATH'] = os.pathsep.join(filter(None, [ env['PYTHONPATH'] = os.pathsep.join(filter(None, [
str(os.getcwd()), env.get('PYTHONPATH', '')])) str(os.getcwd()), env.get('PYTHONPATH', '')]))
kw['env'] = env kw['env'] = env
return subprocess.Popen(cmdargs,
stdout=stdout, stderr=stderr, **kw) popen = subprocess.Popen(cmdargs, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr, **kw)
popen.stdin.close()
return popen
def run(self, *cmdargs): def run(self, *cmdargs):
"""Run a command with arguments. """Run a command with arguments.
@ -914,14 +941,14 @@ def _run(self, *cmdargs):
cmdargs = [str(x) for x in cmdargs] cmdargs = [str(x) for x in cmdargs]
p1 = self.tmpdir.join("stdout") p1 = self.tmpdir.join("stdout")
p2 = self.tmpdir.join("stderr") p2 = self.tmpdir.join("stderr")
print_("running:", ' '.join(cmdargs)) print("running:", ' '.join(cmdargs))
print_(" in:", str(py.path.local())) print(" in:", str(py.path.local()))
f1 = codecs.open(str(p1), "w", encoding="utf8") f1 = codecs.open(str(p1), "w", encoding="utf8")
f2 = codecs.open(str(p2), "w", encoding="utf8") f2 = codecs.open(str(p2), "w", encoding="utf8")
try: try:
now = time.time() now = time.time()
popen = self.popen(cmdargs, stdout=f1, stderr=f2, popen = self.popen(cmdargs, stdout=f1, stderr=f2,
close_fds=(sys.platform != "win32")) close_fds=(sys.platform != "win32"))
ret = popen.wait() ret = popen.wait()
finally: finally:
f1.close() f1.close()
@ -936,19 +963,19 @@ def _run(self, *cmdargs):
f2.close() f2.close()
self._dump_lines(out, sys.stdout) self._dump_lines(out, sys.stdout)
self._dump_lines(err, sys.stderr) self._dump_lines(err, sys.stderr)
return RunResult(ret, out, err, time.time()-now) return RunResult(ret, out, err, time.time() - now)
def _dump_lines(self, lines, fp): def _dump_lines(self, lines, fp):
try: try:
for line in lines: for line in lines:
py.builtin.print_(line, file=fp) print(line, file=fp)
except UnicodeEncodeError: except UnicodeEncodeError:
print("couldn't print to %s because of encoding" % (fp,)) print("couldn't print to %s because of encoding" % (fp,))
def _getpytestargs(self): def _getpytestargs(self):
# we cannot use "(sys.executable,script)" # we cannot use "(sys.executable,script)"
# because on windows the script is e.g. a pytest.exe # because on windows the script is e.g. a pytest.exe
return (sys.executable, _pytest_fullpath,) # noqa return (sys.executable, _pytest_fullpath,) # noqa
def runpython(self, script): def runpython(self, script):
"""Run a python script using sys.executable as interpreter. """Run a python script using sys.executable as interpreter.
@ -975,12 +1002,12 @@ def runpytest_subprocess(self, *args, **kwargs):
""" """
p = py.path.local.make_numbered_dir(prefix="runpytest-", p = py.path.local.make_numbered_dir(prefix="runpytest-",
keep=None, rootdir=self.tmpdir) keep=None, rootdir=self.tmpdir)
args = ('--basetemp=%s' % p, ) + args args = ('--basetemp=%s' % p, ) + args
#for x in args: # for x in args:
# if '--confcutdir' in str(x): # if '--confcutdir' in str(x):
# break # break
#else: # else:
# pass # pass
# args = ('--confcutdir=.',) + args # args = ('--confcutdir=.',) + args
plugins = [x for x in self.plugins if isinstance(x, str)] plugins = [x for x in self.plugins if isinstance(x, str)]
@ -998,7 +1025,7 @@ def spawn_pytest(self, string, expect_timeout=10.0):
The pexpect child is returned. The pexpect child is returned.
""" """
basetemp = self.tmpdir.mkdir("pexpect") basetemp = self.tmpdir.mkdir("temp-pexpect")
invoke = " ".join(map(str, self._getpytestargs())) invoke = " ".join(map(str, self._getpytestargs()))
cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string)
return self.spawn(cmd, expect_timeout=expect_timeout) return self.spawn(cmd, expect_timeout=expect_timeout)
@ -1019,12 +1046,13 @@ def spawn(self, cmd, expect_timeout=10.0):
child.timeout = expect_timeout child.timeout = expect_timeout
return child return child
def getdecoded(out): def getdecoded(out):
try: try:
return out.decode("utf-8") return out.decode("utf-8")
except UnicodeDecodeError: except UnicodeDecodeError:
return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % ( return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (
py.io.saferepr(out),) py.io.saferepr(out),)
class LineComp: class LineComp:
@ -1054,7 +1082,7 @@ class LineMatcher:
""" """
def __init__(self, lines): def __init__(self, lines):
self.lines = lines self.lines = lines
self._log_output = [] self._log_output = []
@ -1093,7 +1121,7 @@ def get_lines_after(self, fnline):
""" """
for i, line in enumerate(self.lines): for i, line in enumerate(self.lines):
if fnline == line or fnmatch(line, fnline): if fnline == line or fnmatch(line, fnline):
return self.lines[i+1:] return self.lines[i + 1:]
raise ValueError("line %r not found in output" % fnline) raise ValueError("line %r not found in output" % fnline)
def _log(self, *args): def _log(self, *args):

File diff suppressed because it is too large Load Diff

626
lib/spack/external/_pytest/python_api.py vendored Normal file
View File

@ -0,0 +1,626 @@
import math
import sys
import py
from _pytest.compat import isclass, izip
from _pytest.outcomes import fail
import _pytest._code
def _cmp_raises_type_error(self, other):
"""__cmp__ implementation which raises TypeError. Used
by Approx base classes to implement only == and != and raise a
TypeError for other comparisons.
Needed in Python 2 only, Python 3 all it takes is not implementing the
other operators at all.
"""
__tracebackhide__ = True
raise TypeError('Comparison operators other than == and != not supported by approx objects')
# builtin pytest.approx helper
class ApproxBase(object):
"""
Provide shared utilities for making approximate comparisons between numbers
or sequences of numbers.
"""
def __init__(self, expected, rel=None, abs=None, nan_ok=False):
self.expected = expected
self.abs = abs
self.rel = rel
self.nan_ok = nan_ok
def __repr__(self):
raise NotImplementedError
def __eq__(self, actual):
return all(
a == self._approx_scalar(x)
for a, x in self._yield_comparisons(actual))
__hash__ = None
def __ne__(self, actual):
return not (actual == self)
if sys.version_info[0] == 2:
__cmp__ = _cmp_raises_type_error
def _approx_scalar(self, x):
return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
def _yield_comparisons(self, actual):
"""
Yield all the pairs of numbers to be compared. This is used to
implement the `__eq__` method.
"""
raise NotImplementedError
class ApproxNumpy(ApproxBase):
"""
Perform approximate comparisons for numpy arrays.
"""
# Tell numpy to use our `__eq__` operator instead of its.
__array_priority__ = 100
def __repr__(self):
# It might be nice to rewrite this function to account for the
# shape of the array...
return "approx({0!r})".format(list(
self._approx_scalar(x) for x in self.expected))
if sys.version_info[0] == 2:
__cmp__ = _cmp_raises_type_error
def __eq__(self, actual):
import numpy as np
try:
actual = np.asarray(actual)
except: # noqa
raise TypeError("cannot compare '{0}' to numpy.ndarray".format(actual))
if actual.shape != self.expected.shape:
return False
return ApproxBase.__eq__(self, actual)
def _yield_comparisons(self, actual):
import numpy as np
# We can be sure that `actual` is a numpy array, because it's
# casted in `__eq__` before being passed to `ApproxBase.__eq__`,
# which is the only method that calls this one.
for i in np.ndindex(self.expected.shape):
yield actual[i], self.expected[i]
class ApproxMapping(ApproxBase):
"""
Perform approximate comparisons for mappings where the values are numbers
(the keys can be anything).
"""
def __repr__(self):
return "approx({0!r})".format(dict(
(k, self._approx_scalar(v))
for k, v in self.expected.items()))
def __eq__(self, actual):
if set(actual.keys()) != set(self.expected.keys()):
return False
return ApproxBase.__eq__(self, actual)
def _yield_comparisons(self, actual):
for k in self.expected.keys():
yield actual[k], self.expected[k]
class ApproxSequence(ApproxBase):
"""
Perform approximate comparisons for sequences of numbers.
"""
# Tell numpy to use our `__eq__` operator instead of its.
__array_priority__ = 100
def __repr__(self):
seq_type = type(self.expected)
if seq_type not in (tuple, list, set):
seq_type = list
return "approx({0!r})".format(seq_type(
self._approx_scalar(x) for x in self.expected))
def __eq__(self, actual):
if len(actual) != len(self.expected):
return False
return ApproxBase.__eq__(self, actual)
def _yield_comparisons(self, actual):
return izip(actual, self.expected)
class ApproxScalar(ApproxBase):
"""
Perform approximate comparisons for single numbers only.
"""
def __repr__(self):
"""
Return a string communicating both the expected value and the tolerance
for the comparison being made, e.g. '1.0 +- 1e-6'. Use the unicode
plus/minus symbol if this is python3 (it's too hard to get right for
python2).
"""
if isinstance(self.expected, complex):
return str(self.expected)
# Infinities aren't compared using tolerances, so don't show a
# tolerance.
if math.isinf(self.expected):
return str(self.expected)
# If a sensible tolerance can't be calculated, self.tolerance will
# raise a ValueError. In this case, display '???'.
try:
vetted_tolerance = '{:.1e}'.format(self.tolerance)
except ValueError:
vetted_tolerance = '???'
if sys.version_info[0] == 2:
return '{0} +- {1}'.format(self.expected, vetted_tolerance)
else:
return u'{0} \u00b1 {1}'.format(self.expected, vetted_tolerance)
def __eq__(self, actual):
"""
Return true if the given value is equal to the expected value within
the pre-specified tolerance.
"""
# Short-circuit exact equality.
if actual == self.expected:
return True
# Allow the user to control whether NaNs are considered equal to each
# other or not. The abs() calls are for compatibility with complex
# numbers.
if math.isnan(abs(self.expected)):
return self.nan_ok and math.isnan(abs(actual))
# Infinity shouldn't be approximately equal to anything but itself, but
# if there's a relative tolerance, it will be infinite and infinity
# will seem approximately equal to everything. The equal-to-itself
# case would have been short circuited above, so here we can just
# return false if the expected value is infinite. The abs() call is
# for compatibility with complex numbers.
if math.isinf(abs(self.expected)):
return False
# Return true if the two numbers are within the tolerance.
return abs(self.expected - actual) <= self.tolerance
__hash__ = None
@property
def tolerance(self):
"""
Return the tolerance for the comparison. This could be either an
absolute tolerance or a relative tolerance, depending on what the user
specified or which would be larger.
"""
def set_default(x, default):
return x if x is not None else default
# Figure out what the absolute tolerance should be. ``self.abs`` is
# either None or a value specified by the user.
absolute_tolerance = set_default(self.abs, 1e-12)
if absolute_tolerance < 0:
raise ValueError("absolute tolerance can't be negative: {0}".format(absolute_tolerance))
if math.isnan(absolute_tolerance):
raise ValueError("absolute tolerance can't be NaN.")
# If the user specified an absolute tolerance but not a relative one,
# just return the absolute tolerance.
if self.rel is None:
if self.abs is not None:
return absolute_tolerance
# Figure out what the relative tolerance should be. ``self.rel`` is
# either None or a value specified by the user. This is done after
# we've made sure the user didn't ask for an absolute tolerance only,
# because we don't want to raise errors about the relative tolerance if
# we aren't even going to use it.
relative_tolerance = set_default(self.rel, 1e-6) * abs(self.expected)
if relative_tolerance < 0:
raise ValueError("relative tolerance can't be negative: {0}".format(absolute_tolerance))
if math.isnan(relative_tolerance):
raise ValueError("relative tolerance can't be NaN.")
# Return the larger of the relative and absolute tolerances.
return max(relative_tolerance, absolute_tolerance)
def approx(expected, rel=None, abs=None, nan_ok=False):
"""
Assert that two numbers (or two sets of numbers) are equal to each other
within some tolerance.
Due to the `intricacies of floating-point arithmetic`__, numbers that we
would intuitively expect to be equal are not always so::
>>> 0.1 + 0.2 == 0.3
False
__ https://docs.python.org/3/tutorial/floatingpoint.html
This problem is commonly encountered when writing tests, e.g. when making
sure that floating-point values are what you expect them to be. One way to
deal with this problem is to assert that two floating-point numbers are
equal to within some appropriate tolerance::
>>> abs((0.1 + 0.2) - 0.3) < 1e-6
True
However, comparisons like this are tedious to write and difficult to
understand. Furthermore, absolute comparisons like the one above are
usually discouraged because there's no tolerance that works well for all
situations. ``1e-6`` is good for numbers around ``1``, but too small for
very big numbers and too big for very small ones. It's better to express
the tolerance as a fraction of the expected value, but relative comparisons
like that are even more difficult to write correctly and concisely.
The ``approx`` class performs floating-point comparisons using a syntax
that's as intuitive as possible::
>>> from pytest import approx
>>> 0.1 + 0.2 == approx(0.3)
True
The same syntax also works for sequences of numbers::
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
True
Dictionary *values*::
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
True
And ``numpy`` arrays::
>>> import numpy as np # doctest: +SKIP
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP
True
By default, ``approx`` considers numbers within a relative tolerance of
``1e-6`` (i.e. one part in a million) of its expected value to be equal.
This treatment would lead to surprising results if the expected value was
``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``.
To handle this case less surprisingly, ``approx`` also considers numbers
within an absolute tolerance of ``1e-12`` of its expected value to be
equal. Infinity and NaN are special cases. Infinity is only considered
equal to itself, regardless of the relative tolerance. NaN is not
considered equal to anything by default, but you can make it be equal to
itself by setting the ``nan_ok`` argument to True. (This is meant to
facilitate comparing arrays that use NaN to mean "no data".)
Both the relative and absolute tolerances can be changed by passing
arguments to the ``approx`` constructor::
>>> 1.0001 == approx(1)
False
>>> 1.0001 == approx(1, rel=1e-3)
True
>>> 1.0001 == approx(1, abs=1e-3)
True
If you specify ``abs`` but not ``rel``, the comparison will not consider
the relative tolerance at all. In other words, two numbers that are within
the default relative tolerance of ``1e-6`` will still be considered unequal
if they exceed the specified absolute tolerance. If you specify both
``abs`` and ``rel``, the numbers will be considered equal if either
tolerance is met::
>>> 1 + 1e-8 == approx(1)
True
>>> 1 + 1e-8 == approx(1, abs=1e-12)
False
>>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
True
If you're thinking about using ``approx``, then you might want to know how
it compares to other good ways of comparing floating-point numbers. All of
these algorithms are based on relative and absolute tolerances and should
agree for the most part, but they do have meaningful differences:
- ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative
tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute
tolerance is met. Because the relative tolerance is calculated w.r.t.
both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor
``b`` is a "reference value"). You have to specify an absolute tolerance
if you want to compare to ``0.0`` because there is no tolerance by
default. Only available in python>=3.5. `More information...`__
__ https://docs.python.org/3/library/math.html#math.isclose
- ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference
between ``a`` and ``b`` is less that the sum of the relative tolerance
w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance
is only calculated w.r.t. ``b``, this test is asymmetric and you can
think of ``b`` as the reference value. Support for comparing sequences
is provided by ``numpy.allclose``. `More information...`__
__ http://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.isclose.html
- ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b``
are within an absolute tolerance of ``1e-7``. No relative tolerance is
considered and the absolute tolerance cannot be changed, so this function
is not appropriate for very large or very small numbers. Also, it's only
available in subclasses of ``unittest.TestCase`` and it's ugly because it
doesn't follow PEP8. `More information...`__
__ https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertAlmostEqual
- ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative
tolerance is met w.r.t. ``b`` or if the absolute tolerance is met.
Because the relative tolerance is only calculated w.r.t. ``b``, this test
is asymmetric and you can think of ``b`` as the reference value. In the
special case that you explicitly specify an absolute tolerance but not a
relative tolerance, only the absolute tolerance is considered.
.. warning::
.. versionchanged:: 3.2
In order to avoid inconsistent behavior, ``TypeError`` is
raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons.
The example below illustrates the problem::
assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10)
assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10)
In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)``
to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to
comparison. This is because the call hierarchy of rich comparisons
follows a fixed behavior. `More information...`__
__ https://docs.python.org/3/reference/datamodel.html#object.__ge__
"""
from collections import Mapping, Sequence
from _pytest.compat import STRING_TYPES as String
# Delegate the comparison to a class that knows how to deal with the type
# of the expected value (e.g. int, float, list, dict, numpy.array, etc).
#
# This architecture is really driven by the need to support numpy arrays.
# The only way to override `==` for arrays without requiring that approx be
# the left operand is to inherit the approx object from `numpy.ndarray`.
# But that can't be a general solution, because it requires (1) numpy to be
# installed and (2) the expected value to be a numpy array. So the general
# solution is to delegate each type of expected value to a different class.
#
# This has the advantage that it made it easy to support mapping types
# (i.e. dict). The old code accepted mapping types, but would only compare
# their keys, which is probably not what most people would expect.
if _is_numpy_array(expected):
cls = ApproxNumpy
elif isinstance(expected, Mapping):
cls = ApproxMapping
elif isinstance(expected, Sequence) and not isinstance(expected, String):
cls = ApproxSequence
else:
cls = ApproxScalar
return cls(expected, rel, abs, nan_ok)
def _is_numpy_array(obj):
"""
Return true if the given object is a numpy array. Make a special effort to
avoid importing numpy unless it's really necessary.
"""
import inspect
for cls in inspect.getmro(type(obj)):
if cls.__module__ == 'numpy':
try:
import numpy as np
return isinstance(obj, np.ndarray)
except ImportError:
pass
return False
# builtin pytest.raises helper
def raises(expected_exception, *args, **kwargs):
"""
Assert that a code block/function call raises ``expected_exception``
and raise a failure exception otherwise.
This helper produces a ``ExceptionInfo()`` object (see below).
If using Python 2.5 or above, you may use this function as a
context manager::
>>> with raises(ZeroDivisionError):
... 1/0
.. versionchanged:: 2.10
In the context manager form you may use the keyword argument
``message`` to specify a custom failure message::
>>> with raises(ZeroDivisionError, message="Expecting ZeroDivisionError"):
... pass
Traceback (most recent call last):
...
Failed: Expecting ZeroDivisionError
.. note::
When using ``pytest.raises`` as a context manager, it's worthwhile to
note that normal context manager rules apply and that the exception
raised *must* be the final line in the scope of the context manager.
Lines of code after that, within the scope of the context manager will
not be executed. For example::
>>> value = 15
>>> with raises(ValueError) as exc_info:
... if value > 10:
... raise ValueError("value must be <= 10")
... assert exc_info.type == ValueError # this will not execute
Instead, the following approach must be taken (note the difference in
scope)::
>>> with raises(ValueError) as exc_info:
... if value > 10:
... raise ValueError("value must be <= 10")
...
>>> assert exc_info.type == ValueError
Since version ``3.1`` you can use the keyword argument ``match`` to assert that the
exception matches a text or regex::
>>> with raises(ValueError, match='must be 0 or None'):
... raise ValueError("value must be 0 or None")
>>> with raises(ValueError, match=r'must be \d+$'):
... raise ValueError("value must be 42")
**Legacy forms**
The forms below are fully supported but are discouraged for new code because the
context manager form is regarded as more readable and less error-prone.
It is possible to specify a callable by passing a to-be-called lambda::
>>> raises(ZeroDivisionError, lambda: 1/0)
<ExceptionInfo ...>
or you can specify an arbitrary callable with arguments::
>>> def f(x): return 1/x
...
>>> raises(ZeroDivisionError, f, 0)
<ExceptionInfo ...>
>>> raises(ZeroDivisionError, f, x=0)
<ExceptionInfo ...>
It is also possible to pass a string to be evaluated at runtime::
>>> raises(ZeroDivisionError, "f(0)")
<ExceptionInfo ...>
The string will be evaluated using the same ``locals()`` and ``globals()``
at the moment of the ``raises`` call.
.. autoclass:: _pytest._code.ExceptionInfo
:members:
.. note::
Similar to caught exception objects in Python, explicitly clearing
local references to returned ``ExceptionInfo`` objects can
help the Python interpreter speed up its garbage collection.
Clearing those references breaks a reference cycle
(``ExceptionInfo`` --> caught exception --> frame stack raising
the exception --> current frame stack --> local variables -->
``ExceptionInfo``) which makes Python keep all objects referenced
from that cycle (including all local variables in the current
frame) alive until the next cyclic garbage collection run. See the
official Python ``try`` statement documentation for more detailed
information.
"""
__tracebackhide__ = True
msg = ("exceptions must be old-style classes or"
" derived from BaseException, not %s")
if isinstance(expected_exception, tuple):
for exc in expected_exception:
if not isclass(exc):
raise TypeError(msg % type(exc))
elif not isclass(expected_exception):
raise TypeError(msg % type(expected_exception))
message = "DID NOT RAISE {0}".format(expected_exception)
match_expr = None
if not args:
if "message" in kwargs:
message = kwargs.pop("message")
if "match" in kwargs:
match_expr = kwargs.pop("match")
message += " matching '{0}'".format(match_expr)
return RaisesContext(expected_exception, message, match_expr)
elif isinstance(args[0], str):
code, = args
assert isinstance(code, str)
frame = sys._getframe(1)
loc = frame.f_locals.copy()
loc.update(kwargs)
# print "raises frame scope: %r" % frame.f_locals
try:
code = _pytest._code.Source(code).compile()
py.builtin.exec_(code, frame.f_globals, loc)
# XXX didn'T mean f_globals == f_locals something special?
# this is destroyed here ...
except expected_exception:
return _pytest._code.ExceptionInfo()
else:
func = args[0]
try:
func(*args[1:], **kwargs)
except expected_exception:
return _pytest._code.ExceptionInfo()
fail(message)
raises.Exception = fail.Exception
class RaisesContext(object):
def __init__(self, expected_exception, message, match_expr):
self.expected_exception = expected_exception
self.message = message
self.match_expr = match_expr
self.excinfo = None
def __enter__(self):
self.excinfo = object.__new__(_pytest._code.ExceptionInfo)
return self.excinfo
def __exit__(self, *tp):
__tracebackhide__ = True
if tp[0] is None:
fail(self.message)
if sys.version_info < (2, 7):
# py26: on __exit__() exc_value often does not contain the
# exception value.
# http://bugs.python.org/issue7853
if not isinstance(tp[1], BaseException):
exc_type, value, traceback = tp
tp = exc_type, exc_type(value), traceback
self.excinfo.__init__(tp)
suppress_exception = issubclass(self.excinfo.type, self.expected_exception)
if sys.version_info[0] == 2 and suppress_exception:
sys.exc_clear()
if self.match_expr:
self.excinfo.match(self.match_expr)
return suppress_exception

View File

@ -1,4 +1,5 @@
""" recording warnings during test function execution. """ """ recording warnings during test function execution. """
from __future__ import absolute_import, division, print_function
import inspect import inspect
@ -6,11 +7,13 @@
import py import py
import sys import sys
import warnings import warnings
import pytest
from _pytest.fixtures import yield_fixture
from _pytest.outcomes import fail
@pytest.yield_fixture @yield_fixture
def recwarn(request): def recwarn():
"""Return a WarningsRecorder instance that provides these methods: """Return a WarningsRecorder instance that provides these methods:
* ``pop(category=None)``: return last warning matching the category. * ``pop(category=None)``: return last warning matching the category.
@ -25,16 +28,9 @@ def recwarn(request):
yield wrec yield wrec
def pytest_namespace():
return {'deprecated_call': deprecated_call,
'warns': warns}
def deprecated_call(func=None, *args, **kwargs): def deprecated_call(func=None, *args, **kwargs):
""" assert that calling ``func(*args, **kwargs)`` triggers a """context manager that can be used to ensure a block of code triggers a
``DeprecationWarning`` or ``PendingDeprecationWarning``. ``DeprecationWarning`` or ``PendingDeprecationWarning``::
This function can be used as a context manager::
>>> import warnings >>> import warnings
>>> def api_call_v2(): >>> def api_call_v2():
@ -44,40 +40,47 @@ def deprecated_call(func=None, *args, **kwargs):
>>> with deprecated_call(): >>> with deprecated_call():
... assert api_call_v2() == 200 ... assert api_call_v2() == 200
Note: we cannot use WarningsRecorder here because it is still subject ``deprecated_call`` can also be used by passing a function and ``*args`` and ``*kwargs``,
to the mechanism that prevents warnings of the same type from being in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
triggered twice for the same module. See #1190. types above.
""" """
if not func: if not func:
return WarningsChecker(expected_warning=DeprecationWarning) return _DeprecatedCallContext()
else:
categories = []
def warn_explicit(message, category, *args, **kwargs):
categories.append(category)
old_warn_explicit(message, category, *args, **kwargs)
def warn(message, category=None, *args, **kwargs):
if isinstance(message, Warning):
categories.append(message.__class__)
else:
categories.append(category)
old_warn(message, category, *args, **kwargs)
old_warn = warnings.warn
old_warn_explicit = warnings.warn_explicit
warnings.warn_explicit = warn_explicit
warnings.warn = warn
try:
ret = func(*args, **kwargs)
finally:
warnings.warn_explicit = old_warn_explicit
warnings.warn = old_warn
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
if not any(issubclass(c, deprecation_categories) for c in categories):
__tracebackhide__ = True __tracebackhide__ = True
raise AssertionError("%r did not produce DeprecationWarning" % (func,)) with _DeprecatedCallContext():
return ret return func(*args, **kwargs)
class _DeprecatedCallContext(object):
"""Implements the logic to capture deprecation warnings as a context manager."""
def __enter__(self):
self._captured_categories = []
self._old_warn = warnings.warn
self._old_warn_explicit = warnings.warn_explicit
warnings.warn_explicit = self._warn_explicit
warnings.warn = self._warn
def _warn_explicit(self, message, category, *args, **kwargs):
self._captured_categories.append(category)
def _warn(self, message, category=None, *args, **kwargs):
if isinstance(message, Warning):
self._captured_categories.append(message.__class__)
else:
self._captured_categories.append(category)
def __exit__(self, exc_type, exc_val, exc_tb):
warnings.warn_explicit = self._old_warn_explicit
warnings.warn = self._old_warn
if exc_type is None:
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
if not any(issubclass(c, deprecation_categories) for c in self._captured_categories):
__tracebackhide__ = True
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
raise AssertionError(msg)
def warns(expected_warning, *args, **kwargs): def warns(expected_warning, *args, **kwargs):
@ -115,24 +118,14 @@ def warns(expected_warning, *args, **kwargs):
return func(*args[1:], **kwargs) return func(*args[1:], **kwargs)
class RecordedWarning(object): class WarningsRecorder(warnings.catch_warnings):
def __init__(self, message, category, filename, lineno, file, line):
self.message = message
self.category = category
self.filename = filename
self.lineno = lineno
self.file = file
self.line = line
class WarningsRecorder(object):
"""A context manager to record raised warnings. """A context manager to record raised warnings.
Adapted from `warnings.catch_warnings`. Adapted from `warnings.catch_warnings`.
""" """
def __init__(self, module=None): def __init__(self):
self._module = sys.modules['warnings'] if module is None else module super(WarningsRecorder, self).__init__(record=True)
self._entered = False self._entered = False
self._list = [] self._list = []
@ -169,38 +162,20 @@ def __enter__(self):
if self._entered: if self._entered:
__tracebackhide__ = True __tracebackhide__ = True
raise RuntimeError("Cannot enter %r twice" % self) raise RuntimeError("Cannot enter %r twice" % self)
self._entered = True self._list = super(WarningsRecorder, self).__enter__()
self._filters = self._module.filters warnings.simplefilter('always')
self._module.filters = self._filters[:]
self._showwarning = self._module.showwarning
def showwarning(message, category, filename, lineno,
file=None, line=None):
self._list.append(RecordedWarning(
message, category, filename, lineno, file, line))
# still perform old showwarning functionality
self._showwarning(
message, category, filename, lineno, file=file, line=line)
self._module.showwarning = showwarning
# allow the same warning to be raised more than once
self._module.simplefilter('always')
return self return self
def __exit__(self, *exc_info): def __exit__(self, *exc_info):
if not self._entered: if not self._entered:
__tracebackhide__ = True __tracebackhide__ = True
raise RuntimeError("Cannot exit %r without entering first" % self) raise RuntimeError("Cannot exit %r without entering first" % self)
self._module.filters = self._filters super(WarningsRecorder, self).__exit__(*exc_info)
self._module.showwarning = self._showwarning
class WarningsChecker(WarningsRecorder): class WarningsChecker(WarningsRecorder):
def __init__(self, expected_warning=None, module=None): def __init__(self, expected_warning=None):
super(WarningsChecker, self).__init__(module=module) super(WarningsChecker, self).__init__()
msg = ("exceptions must be old-style classes or " msg = ("exceptions must be old-style classes or "
"derived from Warning, not %s") "derived from Warning, not %s")
@ -221,6 +196,10 @@ def __exit__(self, *exc_info):
# only check if we're not currently handling an exception # only check if we're not currently handling an exception
if all(a is None for a in exc_info): if all(a is None for a in exc_info):
if self.expected_warning is not None: if self.expected_warning is not None:
if not any(r.category in self.expected_warning for r in self): if not any(issubclass(r.category, self.expected_warning)
for r in self):
__tracebackhide__ = True __tracebackhide__ = True
pytest.fail("DID NOT WARN") fail("DID NOT WARN. No warnings of type {0} was emitted. "
"The list of emitted warnings is: {1}.".format(
self.expected_warning,
[each.message for each in self]))

View File

@ -1,15 +1,18 @@
""" log machine-parseable test session result information in a plain """ log machine-parseable test session result information in a plain
text file. text file.
""" """
from __future__ import absolute_import, division, print_function
import py import py
import os import os
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "resultlog plugin options") group = parser.getgroup("terminal reporting", "resultlog plugin options")
group.addoption('--resultlog', '--result-log', action="store", group.addoption('--resultlog', '--result-log', action="store",
metavar="path", default=None, metavar="path", default=None,
help="DEPRECATED path for machine-readable result log.") help="DEPRECATED path for machine-readable result log.")
def pytest_configure(config): def pytest_configure(config):
resultlog = config.option.resultlog resultlog = config.option.resultlog
@ -18,13 +21,14 @@ def pytest_configure(config):
dirname = os.path.dirname(os.path.abspath(resultlog)) dirname = os.path.dirname(os.path.abspath(resultlog))
if not os.path.isdir(dirname): if not os.path.isdir(dirname):
os.makedirs(dirname) os.makedirs(dirname)
logfile = open(resultlog, 'w', 1) # line buffered logfile = open(resultlog, 'w', 1) # line buffered
config._resultlog = ResultLog(config, logfile) config._resultlog = ResultLog(config, logfile)
config.pluginmanager.register(config._resultlog) config.pluginmanager.register(config._resultlog)
from _pytest.deprecated import RESULT_LOG from _pytest.deprecated import RESULT_LOG
config.warn('C1', RESULT_LOG) config.warn('C1', RESULT_LOG)
def pytest_unconfigure(config): def pytest_unconfigure(config):
resultlog = getattr(config, '_resultlog', None) resultlog = getattr(config, '_resultlog', None)
if resultlog: if resultlog:
@ -32,6 +36,7 @@ def pytest_unconfigure(config):
del config._resultlog del config._resultlog
config.pluginmanager.unregister(resultlog) config.pluginmanager.unregister(resultlog)
def generic_path(item): def generic_path(item):
chain = item.listchain() chain = item.listchain()
gpath = [chain[0].name] gpath = [chain[0].name]
@ -55,15 +60,16 @@ def generic_path(item):
fspath = newfspath fspath = newfspath
return ''.join(gpath) return ''.join(gpath)
class ResultLog(object): class ResultLog(object):
def __init__(self, config, logfile): def __init__(self, config, logfile):
self.config = config self.config = config
self.logfile = logfile # preferably line buffered self.logfile = logfile # preferably line buffered
def write_log_entry(self, testpath, lettercode, longrepr): def write_log_entry(self, testpath, lettercode, longrepr):
py.builtin.print_("%s %s" % (lettercode, testpath), file=self.logfile) print("%s %s" % (lettercode, testpath), file=self.logfile)
for line in longrepr.splitlines(): for line in longrepr.splitlines():
py.builtin.print_(" %s" % line, file=self.logfile) print(" %s" % line, file=self.logfile)
def log_outcome(self, report, lettercode, longrepr): def log_outcome(self, report, lettercode, longrepr):
testpath = getattr(report, 'nodeid', None) testpath = getattr(report, 'nodeid', None)

View File

@ -1,29 +1,26 @@
""" basic collect and runtest protocol implementations """ """ basic collect and runtest protocol implementations """
from __future__ import absolute_import, division, print_function
import bdb import bdb
import os
import sys import sys
from time import time from time import time
import py import py
import pytest from _pytest.compat import _PY2
from _pytest._code.code import TerminalRepr, ExceptionInfo from _pytest._code.code import TerminalRepr, ExceptionInfo
from _pytest.outcomes import skip, Skipped, TEST_OUTCOME
def pytest_namespace():
return {
'fail' : fail,
'skip' : skip,
'importorskip' : importorskip,
'exit' : exit,
}
# #
# pytest plugin hooks # pytest plugin hooks
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "reporting", after="general") group = parser.getgroup("terminal reporting", "reporting", after="general")
group.addoption('--durations', group.addoption('--durations',
action="store", type=int, default=None, metavar="N", action="store", type=int, default=None, metavar="N",
help="show N slowest setup/test durations (N=0 for all)."), help="show N slowest setup/test durations (N=0 for all)."),
def pytest_terminal_summary(terminalreporter): def pytest_terminal_summary(terminalreporter):
durations = terminalreporter.config.option.durations durations = terminalreporter.config.option.durations
@ -48,16 +45,16 @@ def pytest_terminal_summary(terminalreporter):
for rep in dlist: for rep in dlist:
nodeid = rep.nodeid.replace("::()::", "::") nodeid = rep.nodeid.replace("::()::", "::")
tr.write_line("%02.2fs %-8s %s" % tr.write_line("%02.2fs %-8s %s" %
(rep.duration, rep.when, nodeid)) (rep.duration, rep.when, nodeid))
def pytest_sessionstart(session): def pytest_sessionstart(session):
session._setupstate = SetupState() session._setupstate = SetupState()
def pytest_sessionfinish(session): def pytest_sessionfinish(session):
session._setupstate.teardown_all() session._setupstate.teardown_all()
class NodeInfo:
def __init__(self, location):
self.location = location
def pytest_runtest_protocol(item, nextitem): def pytest_runtest_protocol(item, nextitem):
item.ihook.pytest_runtest_logstart( item.ihook.pytest_runtest_logstart(
@ -66,6 +63,7 @@ def pytest_runtest_protocol(item, nextitem):
runtestprotocol(item, nextitem=nextitem) runtestprotocol(item, nextitem=nextitem)
return True return True
def runtestprotocol(item, log=True, nextitem=None): def runtestprotocol(item, log=True, nextitem=None):
hasrequest = hasattr(item, "_request") hasrequest = hasattr(item, "_request")
if hasrequest and not item._request: if hasrequest and not item._request:
@ -78,7 +76,7 @@ def runtestprotocol(item, log=True, nextitem=None):
if not item.config.option.setuponly: if not item.config.option.setuponly:
reports.append(call_and_report(item, "call", log)) reports.append(call_and_report(item, "call", log))
reports.append(call_and_report(item, "teardown", log, reports.append(call_and_report(item, "teardown", log,
nextitem=nextitem)) nextitem=nextitem))
# after all teardown hooks have been called # after all teardown hooks have been called
# want funcargs and request info to go away # want funcargs and request info to go away
if hasrequest: if hasrequest:
@ -86,6 +84,7 @@ def runtestprotocol(item, log=True, nextitem=None):
item.funcargs = None item.funcargs = None
return reports return reports
def show_test_item(item): def show_test_item(item):
"""Show test function, parameters and the fixtures of the test item.""" """Show test function, parameters and the fixtures of the test item."""
tw = item.config.get_terminal_writer() tw = item.config.get_terminal_writer()
@ -96,10 +95,14 @@ def show_test_item(item):
if used_fixtures: if used_fixtures:
tw.write(' (fixtures used: {0})'.format(', '.join(used_fixtures))) tw.write(' (fixtures used: {0})'.format(', '.join(used_fixtures)))
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
_update_current_test_var(item, 'setup')
item.session._setupstate.prepare(item) item.session._setupstate.prepare(item)
def pytest_runtest_call(item): def pytest_runtest_call(item):
_update_current_test_var(item, 'call')
try: try:
item.runtest() item.runtest()
except Exception: except Exception:
@ -112,8 +115,29 @@ def pytest_runtest_call(item):
del tb # Get rid of it in this namespace del tb # Get rid of it in this namespace
raise raise
def pytest_runtest_teardown(item, nextitem): def pytest_runtest_teardown(item, nextitem):
_update_current_test_var(item, 'teardown')
item.session._setupstate.teardown_exact(item, nextitem) item.session._setupstate.teardown_exact(item, nextitem)
_update_current_test_var(item, None)
def _update_current_test_var(item, when):
"""
Update PYTEST_CURRENT_TEST to reflect the current item and stage.
If ``when`` is None, delete PYTEST_CURRENT_TEST from the environment.
"""
var_name = 'PYTEST_CURRENT_TEST'
if when:
value = '{0} ({1})'.format(item.nodeid, when)
if _PY2:
# python 2 doesn't like null bytes on environment variables (see #2644)
value = value.replace('\x00', '(null)')
os.environ[var_name] = value
else:
os.environ.pop(var_name)
def pytest_report_teststatus(report): def pytest_report_teststatus(report):
if report.when in ("setup", "teardown"): if report.when in ("setup", "teardown"):
@ -139,21 +163,25 @@ def call_and_report(item, when, log=True, **kwds):
hook.pytest_exception_interact(node=item, call=call, report=report) hook.pytest_exception_interact(node=item, call=call, report=report)
return report return report
def check_interactive_exception(call, report): def check_interactive_exception(call, report):
return call.excinfo and not ( return call.excinfo and not (
hasattr(report, "wasxfail") or hasattr(report, "wasxfail") or
call.excinfo.errisinstance(skip.Exception) or call.excinfo.errisinstance(skip.Exception) or
call.excinfo.errisinstance(bdb.BdbQuit)) call.excinfo.errisinstance(bdb.BdbQuit))
def call_runtest_hook(item, when, **kwds): def call_runtest_hook(item, when, **kwds):
hookname = "pytest_runtest_" + when hookname = "pytest_runtest_" + when
ihook = getattr(item.ihook, hookname) ihook = getattr(item.ihook, hookname)
return CallInfo(lambda: ihook(item=item, **kwds), when=when) return CallInfo(lambda: ihook(item=item, **kwds), when=when)
class CallInfo: class CallInfo:
""" Result/Exception info a function invocation. """ """ Result/Exception info a function invocation. """
#: None or ExceptionInfo object. #: None or ExceptionInfo object.
excinfo = None excinfo = None
def __init__(self, func, when): def __init__(self, func, when):
#: context of invocation: one of "setup", "call", #: context of invocation: one of "setup", "call",
#: "teardown", "memocollect" #: "teardown", "memocollect"
@ -164,7 +192,7 @@ def __init__(self, func, when):
except KeyboardInterrupt: except KeyboardInterrupt:
self.stop = time() self.stop = time()
raise raise
except: except: # noqa
self.excinfo = ExceptionInfo() self.excinfo = ExceptionInfo()
self.stop = time() self.stop = time()
@ -175,6 +203,7 @@ def __repr__(self):
status = "result: %r" % (self.result,) status = "result: %r" % (self.result,)
return "<CallInfo when=%r %s>" % (self.when, status) return "<CallInfo when=%r %s>" % (self.when, status)
def getslaveinfoline(node): def getslaveinfoline(node):
try: try:
return node._slaveinfocache return node._slaveinfocache
@ -185,6 +214,7 @@ def getslaveinfoline(node):
d['id'], d['sysplatform'], ver, d['executable']) d['id'], d['sysplatform'], ver, d['executable'])
return s return s
class BaseReport(object): class BaseReport(object):
def __init__(self, **kw): def __init__(self, **kw):
@ -249,10 +279,11 @@ def capstderr(self):
def fspath(self): def fspath(self):
return self.nodeid.split("::")[0] return self.nodeid.split("::")[0]
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
when = call.when when = call.when
duration = call.stop-call.start duration = call.stop - call.start
keywords = dict([(x,1) for x in item.keywords]) keywords = dict([(x, 1) for x in item.keywords])
excinfo = call.excinfo excinfo = call.excinfo
sections = [] sections = []
if not call.excinfo: if not call.excinfo:
@ -262,7 +293,7 @@ def pytest_runtest_makereport(item, call):
if not isinstance(excinfo, ExceptionInfo): if not isinstance(excinfo, ExceptionInfo):
outcome = "failed" outcome = "failed"
longrepr = excinfo longrepr = excinfo
elif excinfo.errisinstance(pytest.skip.Exception): elif excinfo.errisinstance(skip.Exception):
outcome = "skipped" outcome = "skipped"
r = excinfo._getreprcrash() r = excinfo._getreprcrash()
longrepr = (str(r.path), r.lineno, r.message) longrepr = (str(r.path), r.lineno, r.message)
@ -270,19 +301,21 @@ def pytest_runtest_makereport(item, call):
outcome = "failed" outcome = "failed"
if call.when == "call": if call.when == "call":
longrepr = item.repr_failure(excinfo) longrepr = item.repr_failure(excinfo)
else: # exception in setup or teardown else: # exception in setup or teardown
longrepr = item._repr_failure_py(excinfo, longrepr = item._repr_failure_py(excinfo,
style=item.config.option.tbstyle) style=item.config.option.tbstyle)
for rwhen, key, content in item._report_sections: for rwhen, key, content in item._report_sections:
sections.append(("Captured %s %s" %(key, rwhen), content)) sections.append(("Captured %s %s" % (key, rwhen), content))
return TestReport(item.nodeid, item.location, return TestReport(item.nodeid, item.location,
keywords, outcome, longrepr, when, keywords, outcome, longrepr, when,
sections, duration) sections, duration)
class TestReport(BaseReport): class TestReport(BaseReport):
""" Basic test report object (also used for setup and teardown calls if """ Basic test report object (also used for setup and teardown calls if
they fail). they fail).
""" """
def __init__(self, nodeid, location, keywords, outcome, def __init__(self, nodeid, location, keywords, outcome,
longrepr, when, sections=(), duration=0, **extra): longrepr, when, sections=(), duration=0, **extra):
#: normalized collection node id #: normalized collection node id
@ -321,16 +354,21 @@ def __repr__(self):
return "<TestReport %r when=%r outcome=%r>" % ( return "<TestReport %r when=%r outcome=%r>" % (
self.nodeid, self.when, self.outcome) self.nodeid, self.when, self.outcome)
class TeardownErrorReport(BaseReport): class TeardownErrorReport(BaseReport):
outcome = "failed" outcome = "failed"
when = "teardown" when = "teardown"
def __init__(self, longrepr, **extra): def __init__(self, longrepr, **extra):
self.longrepr = longrepr self.longrepr = longrepr
self.sections = [] self.sections = []
self.__dict__.update(extra) self.__dict__.update(extra)
def pytest_make_collect_report(collector): def pytest_make_collect_report(collector):
call = CallInfo(collector._memocollect, "memocollect") call = CallInfo(
lambda: list(collector.collect()),
'collect')
longrepr = None longrepr = None
if not call.excinfo: if not call.excinfo:
outcome = "passed" outcome = "passed"
@ -348,7 +386,7 @@ def pytest_make_collect_report(collector):
errorinfo = CollectErrorRepr(errorinfo) errorinfo = CollectErrorRepr(errorinfo)
longrepr = errorinfo longrepr = errorinfo
rep = CollectReport(collector.nodeid, outcome, longrepr, rep = CollectReport(collector.nodeid, outcome, longrepr,
getattr(call, 'result', None)) getattr(call, 'result', None))
rep.call = call # see collect_one_node rep.call = call # see collect_one_node
return rep return rep
@ -369,16 +407,20 @@ def location(self):
def __repr__(self): def __repr__(self):
return "<CollectReport %r lenresult=%s outcome=%r>" % ( return "<CollectReport %r lenresult=%s outcome=%r>" % (
self.nodeid, len(self.result), self.outcome) self.nodeid, len(self.result), self.outcome)
class CollectErrorRepr(TerminalRepr): class CollectErrorRepr(TerminalRepr):
def __init__(self, msg): def __init__(self, msg):
self.longrepr = msg self.longrepr = msg
def toterminal(self, out): def toterminal(self, out):
out.line(self.longrepr, red=True) out.line(self.longrepr, red=True)
class SetupState(object): class SetupState(object):
""" shared state for setting up/tearing down test items or collectors. """ """ shared state for setting up/tearing down test items or collectors. """
def __init__(self): def __init__(self):
self.stack = [] self.stack = []
self._finalizers = {} self._finalizers = {}
@ -390,7 +432,7 @@ def addfinalizer(self, finalizer, colitem):
""" """
assert colitem and not isinstance(colitem, tuple) assert colitem and not isinstance(colitem, tuple)
assert py.builtin.callable(finalizer) assert py.builtin.callable(finalizer)
#assert colitem in self.stack # some unit tests don't setup stack :/ # assert colitem in self.stack # some unit tests don't setup stack :/
self._finalizers.setdefault(colitem, []).append(finalizer) self._finalizers.setdefault(colitem, []).append(finalizer)
def _pop_and_teardown(self): def _pop_and_teardown(self):
@ -404,7 +446,7 @@ def _callfinalizers(self, colitem):
fin = finalizers.pop() fin = finalizers.pop()
try: try:
fin() fin()
except Exception: except TEST_OUTCOME:
# XXX Only first exception will be seen by user, # XXX Only first exception will be seen by user,
# ideally all should be reported. # ideally all should be reported.
if exc is None: if exc is None:
@ -418,7 +460,7 @@ def _teardown_with_finalization(self, colitem):
colitem.teardown() colitem.teardown()
for colitem in self._finalizers: for colitem in self._finalizers:
assert colitem is None or colitem in self.stack \ assert colitem is None or colitem in self.stack \
or isinstance(colitem, tuple) or isinstance(colitem, tuple)
def teardown_all(self): def teardown_all(self):
while self.stack: while self.stack:
@ -451,10 +493,11 @@ def prepare(self, colitem):
self.stack.append(col) self.stack.append(col)
try: try:
col.setup() col.setup()
except Exception: except TEST_OUTCOME:
col._prepare_exc = sys.exc_info() col._prepare_exc = sys.exc_info()
raise raise
def collect_one_node(collector): def collect_one_node(collector):
ihook = collector.ihook ihook = collector.ihook
ihook.pytest_collectstart(collector=collector) ihook.pytest_collectstart(collector=collector)
@ -463,116 +506,3 @@ def collect_one_node(collector):
if call and check_interactive_exception(call, rep): if call and check_interactive_exception(call, rep):
ihook.pytest_exception_interact(node=collector, call=call, report=rep) ihook.pytest_exception_interact(node=collector, call=call, report=rep)
return rep return rep
# =============================================================
# Test OutcomeExceptions and helpers for creating them.
class OutcomeException(Exception):
""" OutcomeException and its subclass instances indicate and
contain info about test and collection outcomes.
"""
def __init__(self, msg=None, pytrace=True):
Exception.__init__(self, msg)
self.msg = msg
self.pytrace = pytrace
def __repr__(self):
if self.msg:
val = self.msg
if isinstance(val, bytes):
val = py._builtin._totext(val, errors='replace')
return val
return "<%s instance>" %(self.__class__.__name__,)
__str__ = __repr__
class Skipped(OutcomeException):
# XXX hackish: on 3k we fake to live in the builtins
# in order to have Skipped exception printing shorter/nicer
__module__ = 'builtins'
def __init__(self, msg=None, pytrace=True, allow_module_level=False):
OutcomeException.__init__(self, msg=msg, pytrace=pytrace)
self.allow_module_level = allow_module_level
class Failed(OutcomeException):
""" raised from an explicit call to pytest.fail() """
__module__ = 'builtins'
class Exit(KeyboardInterrupt):
""" raised for immediate program exits (no tracebacks/summaries)"""
def __init__(self, msg="unknown reason"):
self.msg = msg
KeyboardInterrupt.__init__(self, msg)
# exposed helper methods
def exit(msg):
""" exit testing process as if KeyboardInterrupt was triggered. """
__tracebackhide__ = True
raise Exit(msg)
exit.Exception = Exit
def skip(msg=""):
""" skip an executing test with the given message. Note: it's usually
better to use the pytest.mark.skipif marker to declare a test to be
skipped under certain conditions like mismatching platforms or
dependencies. See the pytest_skipping plugin for details.
"""
__tracebackhide__ = True
raise Skipped(msg=msg)
skip.Exception = Skipped
def fail(msg="", pytrace=True):
""" explicitly fail an currently-executing test with the given Message.
:arg pytrace: if false the msg represents the full failure information
and no python traceback will be reported.
"""
__tracebackhide__ = True
raise Failed(msg=msg, pytrace=pytrace)
fail.Exception = Failed
def importorskip(modname, minversion=None):
""" return imported module if it has at least "minversion" as its
__version__ attribute. If no minversion is specified the a skip
is only triggered if the module can not be imported.
"""
__tracebackhide__ = True
compile(modname, '', 'eval') # to catch syntaxerrors
should_skip = False
try:
__import__(modname)
except ImportError:
# Do not raise chained exception here(#1485)
should_skip = True
if should_skip:
raise Skipped("could not import %r" %(modname,), allow_module_level=True)
mod = sys.modules[modname]
if minversion is None:
return mod
verattr = getattr(mod, '__version__', None)
if minversion is not None:
try:
from pkg_resources import parse_version as pv
except ImportError:
raise Skipped("we have a required version for %r but can not import "
"pkg_resources to parse version strings." % (modname,),
allow_module_level=True)
if verattr is None or pv(verattr) < pv(minversion):
raise Skipped("module %r has __version__ %r, required is: %r" %(
modname, verattr, minversion), allow_module_level=True)
return mod

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, division, print_function
import pytest import pytest
import sys import sys

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, division, print_function
import pytest import pytest

View File

@ -1,18 +1,21 @@
""" support for skip/xfail functions and markers. """ """ support for skip/xfail functions and markers. """
from __future__ import absolute_import, division, print_function
import os import os
import sys import sys
import traceback import traceback
import py import py
import pytest from _pytest.config import hookimpl
from _pytest.mark import MarkInfo, MarkDecorator from _pytest.mark import MarkInfo, MarkDecorator
from _pytest.outcomes import fail, skip, xfail, TEST_OUTCOME
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("general") group = parser.getgroup("general")
group.addoption('--runxfail', group.addoption('--runxfail',
action="store_true", dest="runxfail", default=False, action="store_true", dest="runxfail", default=False,
help="run tests even if they are marked xfail") help="run tests even if they are marked xfail")
parser.addini("xfail_strict", "default for the strict parameter of xfail " parser.addini("xfail_strict", "default for the strict parameter of xfail "
"markers when not given explicitly (default: " "markers when not given explicitly (default: "
@ -23,53 +26,38 @@ def pytest_addoption(parser):
def pytest_configure(config): def pytest_configure(config):
if config.option.runxfail: if config.option.runxfail:
# yay a hack
import pytest
old = pytest.xfail old = pytest.xfail
config._cleanup.append(lambda: setattr(pytest, "xfail", old)) config._cleanup.append(lambda: setattr(pytest, "xfail", old))
def nop(*args, **kwargs): def nop(*args, **kwargs):
pass pass
nop.Exception = XFailed nop.Exception = xfail.Exception
setattr(pytest, "xfail", nop) setattr(pytest, "xfail", nop)
config.addinivalue_line("markers", config.addinivalue_line("markers",
"skip(reason=None): skip the given test function with an optional reason. " "skip(reason=None): skip the given test function with an optional reason. "
"Example: skip(reason=\"no way of currently testing this\") skips the " "Example: skip(reason=\"no way of currently testing this\") skips the "
"test." "test."
) )
config.addinivalue_line("markers", config.addinivalue_line("markers",
"skipif(condition): skip the given test function if eval(condition) " "skipif(condition): skip the given test function if eval(condition) "
"results in a True value. Evaluation happens within the " "results in a True value. Evaluation happens within the "
"module global context. Example: skipif('sys.platform == \"win32\"') " "module global context. Example: skipif('sys.platform == \"win32\"') "
"skips the test if we are on the win32 platform. see " "skips the test if we are on the win32 platform. see "
"http://pytest.org/latest/skipping.html" "http://pytest.org/latest/skipping.html"
) )
config.addinivalue_line("markers", config.addinivalue_line("markers",
"xfail(condition, reason=None, run=True, raises=None, strict=False): " "xfail(condition, reason=None, run=True, raises=None, strict=False): "
"mark the the test function as an expected failure if eval(condition) " "mark the test function as an expected failure if eval(condition) "
"has a True value. Optionally specify a reason for better reporting " "has a True value. Optionally specify a reason for better reporting "
"and run=False if you don't even want to execute the test function. " "and run=False if you don't even want to execute the test function. "
"If only specific exception(s) are expected, you can list them in " "If only specific exception(s) are expected, you can list them in "
"raises, and if the test fails in other ways, it will be reported as " "raises, and if the test fails in other ways, it will be reported as "
"a true failure. See http://pytest.org/latest/skipping.html" "a true failure. See http://pytest.org/latest/skipping.html"
) )
def pytest_namespace():
return dict(xfail=xfail)
class XFailed(pytest.fail.Exception):
""" raised from an explicit call to pytest.xfail() """
def xfail(reason=""):
""" xfail an executing test or setup functions with the given reason."""
__tracebackhide__ = True
raise XFailed(reason)
xfail.Exception = XFailed
class MarkEvaluator: class MarkEvaluator:
@ -97,51 +85,50 @@ def invalidraise(self, exc):
def istrue(self): def istrue(self):
try: try:
return self._istrue() return self._istrue()
except Exception: except TEST_OUTCOME:
self.exc = sys.exc_info() self.exc = sys.exc_info()
if isinstance(self.exc[1], SyntaxError): if isinstance(self.exc[1], SyntaxError):
msg = [" " * (self.exc[1].offset + 4) + "^",] msg = [" " * (self.exc[1].offset + 4) + "^", ]
msg.append("SyntaxError: invalid syntax") msg.append("SyntaxError: invalid syntax")
else: else:
msg = traceback.format_exception_only(*self.exc[:2]) msg = traceback.format_exception_only(*self.exc[:2])
pytest.fail("Error evaluating %r expression\n" fail("Error evaluating %r expression\n"
" %s\n" " %s\n"
"%s" "%s"
%(self.name, self.expr, "\n".join(msg)), % (self.name, self.expr, "\n".join(msg)),
pytrace=False) pytrace=False)
def _getglobals(self): def _getglobals(self):
d = {'os': os, 'sys': sys, 'config': self.item.config} d = {'os': os, 'sys': sys, 'config': self.item.config}
d.update(self.item.obj.__globals__) if hasattr(self.item, 'obj'):
d.update(self.item.obj.__globals__)
return d return d
def _istrue(self): def _istrue(self):
if hasattr(self, 'result'): if hasattr(self, 'result'):
return self.result return self.result
if self.holder: if self.holder:
d = self._getglobals()
if self.holder.args or 'condition' in self.holder.kwargs: if self.holder.args or 'condition' in self.holder.kwargs:
self.result = False self.result = False
# "holder" might be a MarkInfo or a MarkDecorator; only # "holder" might be a MarkInfo or a MarkDecorator; only
# MarkInfo keeps track of all parameters it received in an # MarkInfo keeps track of all parameters it received in an
# _arglist attribute # _arglist attribute
if hasattr(self.holder, '_arglist'): marks = getattr(self.holder, '_marks', None) \
arglist = self.holder._arglist or [self.holder.mark]
else: for _, args, kwargs in marks:
arglist = [(self.holder.args, self.holder.kwargs)]
for args, kwargs in arglist:
if 'condition' in kwargs: if 'condition' in kwargs:
args = (kwargs['condition'],) args = (kwargs['condition'],)
for expr in args: for expr in args:
self.expr = expr self.expr = expr
if isinstance(expr, py.builtin._basestring): if isinstance(expr, py.builtin._basestring):
d = self._getglobals()
result = cached_eval(self.item.config, expr, d) result = cached_eval(self.item.config, expr, d)
else: else:
if "reason" not in kwargs: if "reason" not in kwargs:
# XXX better be checked at collection time # XXX better be checked at collection time
msg = "you need to specify reason=STRING " \ msg = "you need to specify reason=STRING " \
"when using booleans as conditions." "when using booleans as conditions."
pytest.fail(msg) fail(msg)
result = bool(expr) result = bool(expr)
if result: if result:
self.result = True self.result = True
@ -165,7 +152,7 @@ def getexplanation(self):
return expl return expl
@pytest.hookimpl(tryfirst=True) @hookimpl(tryfirst=True)
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
# Check if skip or skipif are specified as pytest marks # Check if skip or skipif are specified as pytest marks
@ -174,23 +161,23 @@ def pytest_runtest_setup(item):
eval_skipif = MarkEvaluator(item, 'skipif') eval_skipif = MarkEvaluator(item, 'skipif')
if eval_skipif.istrue(): if eval_skipif.istrue():
item._evalskip = eval_skipif item._evalskip = eval_skipif
pytest.skip(eval_skipif.getexplanation()) skip(eval_skipif.getexplanation())
skip_info = item.keywords.get('skip') skip_info = item.keywords.get('skip')
if isinstance(skip_info, (MarkInfo, MarkDecorator)): if isinstance(skip_info, (MarkInfo, MarkDecorator)):
item._evalskip = True item._evalskip = True
if 'reason' in skip_info.kwargs: if 'reason' in skip_info.kwargs:
pytest.skip(skip_info.kwargs['reason']) skip(skip_info.kwargs['reason'])
elif skip_info.args: elif skip_info.args:
pytest.skip(skip_info.args[0]) skip(skip_info.args[0])
else: else:
pytest.skip("unconditional skip") skip("unconditional skip")
item._evalxfail = MarkEvaluator(item, 'xfail') item._evalxfail = MarkEvaluator(item, 'xfail')
check_xfail_no_run(item) check_xfail_no_run(item)
@pytest.mark.hookwrapper @hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem): def pytest_pyfunc_call(pyfuncitem):
check_xfail_no_run(pyfuncitem) check_xfail_no_run(pyfuncitem)
outcome = yield outcome = yield
@ -205,7 +192,7 @@ def check_xfail_no_run(item):
evalxfail = item._evalxfail evalxfail = item._evalxfail
if evalxfail.istrue(): if evalxfail.istrue():
if not evalxfail.get('run', True): if not evalxfail.get('run', True):
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation()) xfail("[NOTRUN] " + evalxfail.getexplanation())
def check_strict_xfail(pyfuncitem): def check_strict_xfail(pyfuncitem):
@ -217,10 +204,10 @@ def check_strict_xfail(pyfuncitem):
if is_strict_xfail: if is_strict_xfail:
del pyfuncitem._evalxfail del pyfuncitem._evalxfail
explanation = evalxfail.getexplanation() explanation = evalxfail.getexplanation()
pytest.fail('[XPASS(strict)] ' + explanation, pytrace=False) fail('[XPASS(strict)] ' + explanation, pytrace=False)
@pytest.hookimpl(hookwrapper=True) @hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
outcome = yield outcome = yield
rep = outcome.get_result() rep = outcome.get_result()
@ -240,11 +227,11 @@ def pytest_runtest_makereport(item, call):
rep.wasxfail = rep.longrepr rep.wasxfail = rep.longrepr
elif item.config.option.runxfail: elif item.config.option.runxfail:
pass # don't interefere pass # don't interefere
elif call.excinfo and call.excinfo.errisinstance(pytest.xfail.Exception): elif call.excinfo and call.excinfo.errisinstance(xfail.Exception):
rep.wasxfail = "reason: " + call.excinfo.value.msg rep.wasxfail = "reason: " + call.excinfo.value.msg
rep.outcome = "skipped" rep.outcome = "skipped"
elif evalxfail and not rep.skipped and evalxfail.wasvalid() and \ elif evalxfail and not rep.skipped and evalxfail.wasvalid() and \
evalxfail.istrue(): evalxfail.istrue():
if call.excinfo: if call.excinfo:
if evalxfail.invalidraise(call.excinfo.value): if evalxfail.invalidraise(call.excinfo.value):
rep.outcome = "failed" rep.outcome = "failed"
@ -270,6 +257,8 @@ def pytest_runtest_makereport(item, call):
rep.longrepr = filename, line, reason rep.longrepr = filename, line, reason
# called by terminalreporter progress reporting # called by terminalreporter progress reporting
def pytest_report_teststatus(report): def pytest_report_teststatus(report):
if hasattr(report, "wasxfail"): if hasattr(report, "wasxfail"):
if report.skipped: if report.skipped:
@ -278,10 +267,12 @@ def pytest_report_teststatus(report):
return "xpassed", "X", ("XPASS", {'yellow': True}) return "xpassed", "X", ("XPASS", {'yellow': True})
# called by the terminalreporter instance/plugin # called by the terminalreporter instance/plugin
def pytest_terminal_summary(terminalreporter): def pytest_terminal_summary(terminalreporter):
tr = terminalreporter tr = terminalreporter
if not tr.reportchars: if not tr.reportchars:
#for name in "xfailed skipped failed xpassed": # for name in "xfailed skipped failed xpassed":
# if not tr.stats.get(name, 0): # if not tr.stats.get(name, 0):
# tr.write_line("HINT: use '-r' option to see extra " # tr.write_line("HINT: use '-r' option to see extra "
# "summary info about tests") # "summary info about tests")
@ -308,12 +299,14 @@ def pytest_terminal_summary(terminalreporter):
for line in lines: for line in lines:
tr._tw.line(line) tr._tw.line(line)
def show_simple(terminalreporter, lines, stat, format): def show_simple(terminalreporter, lines, stat, format):
failed = terminalreporter.stats.get(stat) failed = terminalreporter.stats.get(stat)
if failed: if failed:
for rep in failed: for rep in failed:
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid) pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
lines.append(format %(pos,)) lines.append(format % (pos,))
def show_xfailed(terminalreporter, lines): def show_xfailed(terminalreporter, lines):
xfailed = terminalreporter.stats.get("xfailed") xfailed = terminalreporter.stats.get("xfailed")
@ -325,13 +318,15 @@ def show_xfailed(terminalreporter, lines):
if reason: if reason:
lines.append(" " + str(reason)) lines.append(" " + str(reason))
def show_xpassed(terminalreporter, lines): def show_xpassed(terminalreporter, lines):
xpassed = terminalreporter.stats.get("xpassed") xpassed = terminalreporter.stats.get("xpassed")
if xpassed: if xpassed:
for rep in xpassed: for rep in xpassed:
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid) pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail reason = rep.wasxfail
lines.append("XPASS %s %s" %(pos, reason)) lines.append("XPASS %s %s" % (pos, reason))
def cached_eval(config, expr, d): def cached_eval(config, expr, d):
if not hasattr(config, '_evalcache'): if not hasattr(config, '_evalcache'):
@ -351,25 +346,27 @@ def folded_skips(skipped):
key = event.longrepr key = event.longrepr
assert len(key) == 3, (event, key) assert len(key) == 3, (event, key)
d.setdefault(key, []).append(event) d.setdefault(key, []).append(event)
l = [] values = []
for key, events in d.items(): for key, events in d.items():
l.append((len(events),) + key) values.append((len(events),) + key)
return l return values
def show_skipped(terminalreporter, lines): def show_skipped(terminalreporter, lines):
tr = terminalreporter tr = terminalreporter
skipped = tr.stats.get('skipped', []) skipped = tr.stats.get('skipped', [])
if skipped: if skipped:
#if not tr.hasopt('skipped'): # if not tr.hasopt('skipped'):
# tr.write_line( # tr.write_line(
# "%d skipped tests, specify -rs for more info" % # "%d skipped tests, specify -rs for more info" %
# len(skipped)) # len(skipped))
# return # return
fskips = folded_skips(skipped) fskips = folded_skips(skipped)
if fskips: if fskips:
#tr.write_sep("_", "skipped test summary") # tr.write_sep("_", "skipped test summary")
for num, fspath, lineno, reason in fskips: for num, fspath, lineno, reason in fskips:
if reason.startswith("Skipped: "): if reason.startswith("Skipped: "):
reason = reason[9:] reason = reason[9:]
lines.append("SKIP [%d] %s:%d: %s" % lines.append(
(num, fspath, lineno, reason)) "SKIP [%d] %s:%d: %s" %
(num, fspath, lineno + 1, reason))

View File

@ -2,6 +2,9 @@
This is a good source for looking at the various reporting hooks. This is a good source for looking at the various reporting hooks.
""" """
from __future__ import absolute_import, division, print_function
import itertools
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
import pytest import pytest
@ -10,39 +13,41 @@
import time import time
import platform import platform
from _pytest import nodes
import _pytest._pluggy as pluggy import _pytest._pluggy as pluggy
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "reporting", after="general") group = parser.getgroup("terminal reporting", "reporting", after="general")
group._addoption('-v', '--verbose', action="count", group._addoption('-v', '--verbose', action="count",
dest="verbose", default=0, help="increase verbosity."), dest="verbose", default=0, help="increase verbosity."),
group._addoption('-q', '--quiet', action="count", group._addoption('-q', '--quiet', action="count",
dest="quiet", default=0, help="decrease verbosity."), dest="quiet", default=0, help="decrease verbosity."),
group._addoption('-r', group._addoption('-r',
action="store", dest="reportchars", default='', metavar="chars", action="store", dest="reportchars", default='', metavar="chars",
help="show extra test summary info as specified by chars (f)ailed, " help="show extra test summary info as specified by chars (f)ailed, "
"(E)error, (s)skipped, (x)failed, (X)passed, " "(E)error, (s)skipped, (x)failed, (X)passed, "
"(p)passed, (P)passed with output, (a)all except pP. " "(p)passed, (P)passed with output, (a)all except pP. "
"The pytest warnings are displayed at all times except when " "Warnings are displayed at all times except when "
"--disable-pytest-warnings is set") "--disable-warnings is set")
group._addoption('--disable-pytest-warnings', default=False, group._addoption('--disable-warnings', '--disable-pytest-warnings', default=False,
dest='disablepytestwarnings', action='store_true', dest='disable_warnings', action='store_true',
help='disable warnings summary, overrides -r w flag') help='disable warnings summary')
group._addoption('-l', '--showlocals', group._addoption('-l', '--showlocals',
action="store_true", dest="showlocals", default=False, action="store_true", dest="showlocals", default=False,
help="show locals in tracebacks (disabled by default).") help="show locals in tracebacks (disabled by default).")
group._addoption('--tb', metavar="style", group._addoption('--tb', metavar="style",
action="store", dest="tbstyle", default='auto', action="store", dest="tbstyle", default='auto',
choices=['auto', 'long', 'short', 'no', 'line', 'native'], choices=['auto', 'long', 'short', 'no', 'line', 'native'],
help="traceback print mode (auto/long/short/line/native/no).") help="traceback print mode (auto/long/short/line/native/no).")
group._addoption('--fulltrace', '--full-trace', group._addoption('--fulltrace', '--full-trace',
action="store_true", default=False, action="store_true", default=False,
help="don't cut any tracebacks (default is to cut).") help="don't cut any tracebacks (default is to cut).")
group._addoption('--color', metavar="color", group._addoption('--color', metavar="color",
action="store", dest="color", default='auto', action="store", dest="color", default='auto',
choices=['yes', 'no', 'auto'], choices=['yes', 'no', 'auto'],
help="color terminal output (yes/no/auto).") help="color terminal output (yes/no/auto).")
def pytest_configure(config): def pytest_configure(config):
config.option.verbose -= config.option.quiet config.option.verbose -= config.option.quiet
@ -54,12 +59,13 @@ def mywriter(tags, args):
reporter.write_line("[traceconfig] " + msg) reporter.write_line("[traceconfig] " + msg)
config.trace.root.setprocessor("pytest:config", mywriter) config.trace.root.setprocessor("pytest:config", mywriter)
def getreportopt(config): def getreportopt(config):
reportopts = "" reportopts = ""
reportchars = config.option.reportchars reportchars = config.option.reportchars
if not config.option.disablepytestwarnings and 'w' not in reportchars: if not config.option.disable_warnings and 'w' not in reportchars:
reportchars += 'w' reportchars += 'w'
elif config.option.disablepytestwarnings and 'w' in reportchars: elif config.option.disable_warnings and 'w' in reportchars:
reportchars = reportchars.replace('w', '') reportchars = reportchars.replace('w', '')
if reportchars: if reportchars:
for char in reportchars: for char in reportchars:
@ -69,6 +75,7 @@ def getreportopt(config):
reportopts = 'fEsxXw' reportopts = 'fEsxXw'
return reportopts return reportopts
def pytest_report_teststatus(report): def pytest_report_teststatus(report):
if report.passed: if report.passed:
letter = "." letter = "."
@ -80,13 +87,41 @@ def pytest_report_teststatus(report):
letter = "f" letter = "f"
return report.outcome, letter, report.outcome.upper() return report.outcome, letter, report.outcome.upper()
class WarningReport: class WarningReport:
"""
Simple structure to hold warnings information captured by ``pytest_logwarning``.
"""
def __init__(self, code, message, nodeid=None, fslocation=None): def __init__(self, code, message, nodeid=None, fslocation=None):
"""
:param code: unused
:param str message: user friendly message about the warning
:param str|None nodeid: node id that generated the warning (see ``get_location``).
:param tuple|py.path.local fslocation:
file system location of the source of the warning (see ``get_location``).
"""
self.code = code self.code = code
self.message = message self.message = message
self.nodeid = nodeid self.nodeid = nodeid
self.fslocation = fslocation self.fslocation = fslocation
def get_location(self, config):
"""
Returns the more user-friendly information about the location
of a warning, or None.
"""
if self.nodeid:
return self.nodeid
if self.fslocation:
if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
filename, linenum = self.fslocation[:2]
relpath = py.path.local(filename).relto(config.invocation_dir)
return '%s:%s' % (relpath, linenum)
else:
return str(self.fslocation)
return None
class TerminalReporter: class TerminalReporter:
def __init__(self, config, file=None): def __init__(self, config, file=None):
@ -146,8 +181,22 @@ def write_line(self, line, **markup):
self._tw.line(line, **markup) self._tw.line(line, **markup)
def rewrite(self, line, **markup): def rewrite(self, line, **markup):
"""
Rewinds the terminal cursor to the beginning and writes the given line.
:kwarg erase: if True, will also add spaces until the full terminal width to ensure
previous lines are properly erased.
The rest of the keyword arguments are markup instructions.
"""
erase = markup.pop('erase', False)
if erase:
fill_count = self._tw.fullwidth - len(line)
fill = ' ' * fill_count
else:
fill = ''
line = str(line) line = str(line)
self._tw.write("\r" + line, **markup) self._tw.write("\r" + line + fill, **markup)
def write_sep(self, sep, title=None, **markup): def write_sep(self, sep, title=None, **markup):
self.ensure_newline() self.ensure_newline()
@ -166,8 +215,6 @@ def pytest_internalerror(self, excrepr):
def pytest_logwarning(self, code, fslocation, message, nodeid): def pytest_logwarning(self, code, fslocation, message, nodeid):
warnings = self.stats.setdefault("warnings", []) warnings = self.stats.setdefault("warnings", [])
if isinstance(fslocation, tuple):
fslocation = "%s:%d" % fslocation
warning = WarningReport(code=code, fslocation=fslocation, warning = WarningReport(code=code, fslocation=fslocation,
message=message, nodeid=nodeid) message=message, nodeid=nodeid)
warnings.append(warning) warnings.append(warning)
@ -212,15 +259,15 @@ def pytest_runtest_logreport(self, report):
word, markup = word word, markup = word
else: else:
if rep.passed: if rep.passed:
markup = {'green':True} markup = {'green': True}
elif rep.failed: elif rep.failed:
markup = {'red':True} markup = {'red': True}
elif rep.skipped: elif rep.skipped:
markup = {'yellow':True} markup = {'yellow': True}
line = self._locationline(rep.nodeid, *rep.location) line = self._locationline(rep.nodeid, *rep.location)
if not hasattr(rep, 'node'): if not hasattr(rep, 'node'):
self.write_ensure_prefix(line, word, **markup) self.write_ensure_prefix(line, word, **markup)
#self._tw.write(word, **markup) # self._tw.write(word, **markup)
else: else:
self.ensure_newline() self.ensure_newline()
if hasattr(rep, 'node'): if hasattr(rep, 'node'):
@ -241,7 +288,7 @@ def pytest_collectreport(self, report):
items = [x for x in report.result if isinstance(x, pytest.Item)] items = [x for x in report.result if isinstance(x, pytest.Item)]
self._numcollected += len(items) self._numcollected += len(items)
if self.isatty: if self.isatty:
#self.write_fspath_result(report.nodeid, 'E') # self.write_fspath_result(report.nodeid, 'E')
self.report_collect() self.report_collect()
def report_collect(self, final=False): def report_collect(self, final=False):
@ -254,15 +301,15 @@ def report_collect(self, final=False):
line = "collected " line = "collected "
else: else:
line = "collecting " line = "collecting "
line += str(self._numcollected) + " items" line += str(self._numcollected) + " item" + ('' if self._numcollected == 1 else 's')
if errors: if errors:
line += " / %d errors" % errors line += " / %d errors" % errors
if skipped: if skipped:
line += " / %d skipped" % skipped line += " / %d skipped" % skipped
if self.isatty: if self.isatty:
self.rewrite(line, bold=True, erase=True)
if final: if final:
line += " \n" self.write('\n')
self.rewrite(line, bold=True)
else: else:
self.write_line(line) self.write_line(line)
@ -288,6 +335,9 @@ def pytest_sessionstart(self, session):
self.write_line(msg) self.write_line(msg)
lines = self.config.hook.pytest_report_header( lines = self.config.hook.pytest_report_header(
config=self.config, startdir=self.startdir) config=self.config, startdir=self.startdir)
self._write_report_lines_from_hooks(lines)
def _write_report_lines_from_hooks(self, lines):
lines.reverse() lines.reverse()
for line in flatten(lines): for line in flatten(lines):
self.write_line(line) self.write_line(line)
@ -295,8 +345,8 @@ def pytest_sessionstart(self, session):
def pytest_report_header(self, config): def pytest_report_header(self, config):
inifile = "" inifile = ""
if config.inifile: if config.inifile:
inifile = config.rootdir.bestrelpath(config.inifile) inifile = " " + config.rootdir.bestrelpath(config.inifile)
lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)] lines = ["rootdir: %s, inifile:%s" % (config.rootdir, inifile)]
plugininfo = config.pluginmanager.list_plugin_distinfo() plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo: if plugininfo:
@ -314,10 +364,9 @@ def pytest_collection_finish(self, session):
rep.toterminal(self._tw) rep.toterminal(self._tw)
return 1 return 1
return 0 return 0
if not self.showheader: lines = self.config.hook.pytest_report_collectionfinish(
return config=self.config, startdir=self.startdir, items=session.items)
#for i, testarg in enumerate(self.config.args): self._write_report_lines_from_hooks(lines)
# self.write_line("test path %d: %s" %(i+1, testarg))
def _printcollecteditems(self, items): def _printcollecteditems(self, items):
# to print out items and their parent collectors # to print out items and their parent collectors
@ -340,14 +389,14 @@ def _printcollecteditems(self, items):
stack = [] stack = []
indent = "" indent = ""
for item in items: for item in items:
needed_collectors = item.listchain()[1:] # strip root node needed_collectors = item.listchain()[1:] # strip root node
while stack: while stack:
if stack == needed_collectors[:len(stack)]: if stack == needed_collectors[:len(stack)]:
break break
stack.pop() stack.pop()
for col in needed_collectors[len(stack):]: for col in needed_collectors[len(stack):]:
stack.append(col) stack.append(col)
#if col.name == "()": # if col.name == "()":
# continue # continue
indent = (len(stack) - 1) * " " indent = (len(stack) - 1) * " "
self._tw.line("%s%s" % (indent, col)) self._tw.line("%s%s" % (indent, col))
@ -396,15 +445,15 @@ def mkrel(nodeid):
line = self.config.cwd_relative_nodeid(nodeid) line = self.config.cwd_relative_nodeid(nodeid)
if domain and line.endswith(domain): if domain and line.endswith(domain):
line = line[:-len(domain)] line = line[:-len(domain)]
l = domain.split("[") values = domain.split("[")
l[0] = l[0].replace('.', '::') # don't replace '.' in params values[0] = values[0].replace('.', '::') # don't replace '.' in params
line += "[".join(l) line += "[".join(values)
return line return line
# collect_fspath comes from testid which has a "/"-normalized path # collect_fspath comes from testid which has a "/"-normalized path
if fspath: if fspath:
res = mkrel(nodeid).replace("::()", "") # parens-normalization res = mkrel(nodeid).replace("::()", "") # parens-normalization
if nodeid.split("::")[0] != fspath.replace("\\", "/"): if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP):
res += " <- " + self.startdir.bestrelpath(fspath) res += " <- " + self.startdir.bestrelpath(fspath)
else: else:
res = "[location]" res = "[location]"
@ -415,7 +464,7 @@ def _getfailureheadline(self, rep):
fspath, lineno, domain = rep.location fspath, lineno, domain = rep.location
return domain return domain
else: else:
return "test session" # XXX? return "test session" # XXX?
def _getcrashline(self, rep): def _getcrashline(self, rep):
try: try:
@ -430,21 +479,29 @@ def _getcrashline(self, rep):
# summaries for sessionfinish # summaries for sessionfinish
# #
def getreports(self, name): def getreports(self, name):
l = [] values = []
for x in self.stats.get(name, []): for x in self.stats.get(name, []):
if not hasattr(x, '_pdbshown'): if not hasattr(x, '_pdbshown'):
l.append(x) values.append(x)
return l return values
def summary_warnings(self): def summary_warnings(self):
if self.hasopt("w"): if self.hasopt("w"):
warnings = self.stats.get("warnings") all_warnings = self.stats.get("warnings")
if not warnings: if not all_warnings:
return return
self.write_sep("=", "pytest-warning summary")
for w in warnings: grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config))
self._tw.line("W%s %s %s" % (w.code,
w.fslocation, w.message)) self.write_sep("=", "warnings summary", yellow=True, bold=False)
for location, warnings in grouped:
self._tw.line(str(location) or '<undetermined location>')
for w in warnings:
lines = w.message.splitlines()
indented = '\n'.join(' ' + x for x in lines)
self._tw.line(indented)
self._tw.line()
self._tw.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html')
def summary_passes(self): def summary_passes(self):
if self.config.option.tbstyle != "no": if self.config.option.tbstyle != "no":
@ -466,7 +523,6 @@ def print_teardown_sections(self, rep):
content = content[:-1] content = content[:-1]
self._tw.line(content) self._tw.line(content)
def summary_failures(self): def summary_failures(self):
if self.config.option.tbstyle != "no": if self.config.option.tbstyle != "no":
reports = self.getreports('failed') reports = self.getreports('failed')
@ -528,6 +584,7 @@ def summary_deselected(self):
self.write_sep("=", "%d tests deselected" % ( self.write_sep("=", "%d tests deselected" % (
len(self.stats['deselected'])), bold=True) len(self.stats['deselected'])), bold=True)
def repr_pythonversion(v=None): def repr_pythonversion(v=None):
if v is None: if v is None:
v = sys.version_info v = sys.version_info
@ -536,30 +593,30 @@ def repr_pythonversion(v=None):
except (TypeError, ValueError): except (TypeError, ValueError):
return str(v) return str(v)
def flatten(l):
for x in l: def flatten(values):
for x in values:
if isinstance(x, (list, tuple)): if isinstance(x, (list, tuple)):
for y in flatten(x): for y in flatten(x):
yield y yield y
else: else:
yield x yield x
def build_summary_stats_line(stats): def build_summary_stats_line(stats):
keys = ("failed passed skipped deselected " keys = ("failed passed skipped deselected "
"xfailed xpassed warnings error").split() "xfailed xpassed warnings error").split()
key_translation = {'warnings': 'pytest-warnings'}
unknown_key_seen = False unknown_key_seen = False
for key in stats.keys(): for key in stats.keys():
if key not in keys: if key not in keys:
if key: # setup/teardown reports have an empty key, ignore them if key: # setup/teardown reports have an empty key, ignore them
keys.append(key) keys.append(key)
unknown_key_seen = True unknown_key_seen = True
parts = [] parts = []
for key in keys: for key in keys:
val = stats.get(key, None) val = stats.get(key, None)
if val: if val:
key_name = key_translation.get(key, key) parts.append("%d %s" % (len(val), key))
parts.append("%d %s" % (len(val), key_name))
if parts: if parts:
line = ", ".join(parts) line = ", ".join(parts)
@ -579,7 +636,7 @@ def build_summary_stats_line(stats):
def _plugin_nameversions(plugininfo): def _plugin_nameversions(plugininfo):
l = [] values = []
for plugin, dist in plugininfo: for plugin, dist in plugininfo:
# gets us name and version! # gets us name and version!
name = '{dist.project_name}-{dist.version}'.format(dist=dist) name = '{dist.project_name}-{dist.version}'.format(dist=dist)
@ -588,6 +645,6 @@ def _plugin_nameversions(plugininfo):
name = name[7:] name = name[7:]
# we decided to print python package names # we decided to print python package names
# they can have more than one plugin # they can have more than one plugin
if name not in l: if name not in values:
l.append(name) values.append(name)
return l return values

View File

@ -1,4 +1,6 @@
""" support for providing temporary directories to test functions. """ """ support for providing temporary directories to test functions. """
from __future__ import absolute_import, division, print_function
import re import re
import pytest import pytest
@ -23,7 +25,7 @@ def ensuretemp(self, string, dir=1):
provides an empty unique-per-test-invocation directory provides an empty unique-per-test-invocation directory
and is guaranteed to be empty. and is guaranteed to be empty.
""" """
#py.log._apiwarn(">1.1", "use tmpdir function argument") # py.log._apiwarn(">1.1", "use tmpdir function argument")
return self.getbasetemp().ensure(string, dir=dir) return self.getbasetemp().ensure(string, dir=dir)
def mktemp(self, basename, numbered=True): def mktemp(self, basename, numbered=True):
@ -36,7 +38,7 @@ def mktemp(self, basename, numbered=True):
p = basetemp.mkdir(basename) p = basetemp.mkdir(basename)
else: else:
p = py.path.local.make_numbered_dir(prefix=basename, p = py.path.local.make_numbered_dir(prefix=basename,
keep=0, rootdir=basetemp, lock_timeout=None) keep=0, rootdir=basetemp, lock_timeout=None)
self.trace("mktemp", p) self.trace("mktemp", p)
return p return p
@ -116,7 +118,7 @@ def tmpdir(request, tmpdir_factory):
path object. path object.
""" """
name = request.node.name name = request.node.name
name = re.sub("[\W]", "_", name) name = re.sub(r"[\W]", "_", name)
MAXVAL = 30 MAXVAL = 30
if len(name) > MAXVAL: if len(name) > MAXVAL:
name = name[:MAXVAL] name = name[:MAXVAL]

View File

@ -1,13 +1,14 @@
""" discovery and running of std-library "unittest" style tests. """ """ discovery and running of std-library "unittest" style tests. """
from __future__ import absolute_import from __future__ import absolute_import, division, print_function
import sys import sys
import traceback import traceback
import pytest # for transferring markers
# for transfering markers
import _pytest._code import _pytest._code
from _pytest.python import transfer_markers from _pytest.config import hookimpl
from _pytest.outcomes import fail, skip, xfail
from _pytest.python import transfer_markers, Class, Module, Function
from _pytest.skipping import MarkEvaluator from _pytest.skipping import MarkEvaluator
@ -22,11 +23,11 @@ def pytest_pycollect_makeitem(collector, name, obj):
return UnitTestCase(name, parent=collector) return UnitTestCase(name, parent=collector)
class UnitTestCase(pytest.Class): class UnitTestCase(Class):
# marker for fixturemanger.getfixtureinfo() # marker for fixturemanger.getfixtureinfo()
# to declare that our children do not support funcargs # to declare that our children do not support funcargs
nofuncargs = True nofuncargs = True
def setup(self): def setup(self):
cls = self.obj cls = self.obj
if getattr(cls, '__unittest_skip__', False): if getattr(cls, '__unittest_skip__', False):
@ -46,7 +47,7 @@ def collect(self):
return return
self.session._fixturemanager.parsefactories(self, unittest=True) self.session._fixturemanager.parsefactories(self, unittest=True)
loader = TestLoader() loader = TestLoader()
module = self.getparent(pytest.Module).obj module = self.getparent(Module).obj
foundsomething = False foundsomething = False
for name in loader.getTestCaseNames(self.obj): for name in loader.getTestCaseNames(self.obj):
x = getattr(self.obj, name) x = getattr(self.obj, name)
@ -65,8 +66,7 @@ def collect(self):
yield TestCaseFunction('runTest', parent=self) yield TestCaseFunction('runTest', parent=self)
class TestCaseFunction(Function):
class TestCaseFunction(pytest.Function):
_excinfo = None _excinfo = None
def setup(self): def setup(self):
@ -109,38 +109,39 @@ def _addexcinfo(self, rawexcinfo):
except TypeError: except TypeError:
try: try:
try: try:
l = traceback.format_exception(*rawexcinfo) values = traceback.format_exception(*rawexcinfo)
l.insert(0, "NOTE: Incompatible Exception Representation, " values.insert(0, "NOTE: Incompatible Exception Representation, "
"displaying natively:\n\n") "displaying natively:\n\n")
pytest.fail("".join(l), pytrace=False) fail("".join(values), pytrace=False)
except (pytest.fail.Exception, KeyboardInterrupt): except (fail.Exception, KeyboardInterrupt):
raise raise
except: except: # noqa
pytest.fail("ERROR: Unknown Incompatible Exception " fail("ERROR: Unknown Incompatible Exception "
"representation:\n%r" %(rawexcinfo,), pytrace=False) "representation:\n%r" % (rawexcinfo,), pytrace=False)
except KeyboardInterrupt: except KeyboardInterrupt:
raise raise
except pytest.fail.Exception: except fail.Exception:
excinfo = _pytest._code.ExceptionInfo() excinfo = _pytest._code.ExceptionInfo()
self.__dict__.setdefault('_excinfo', []).append(excinfo) self.__dict__.setdefault('_excinfo', []).append(excinfo)
def addError(self, testcase, rawexcinfo): def addError(self, testcase, rawexcinfo):
self._addexcinfo(rawexcinfo) self._addexcinfo(rawexcinfo)
def addFailure(self, testcase, rawexcinfo): def addFailure(self, testcase, rawexcinfo):
self._addexcinfo(rawexcinfo) self._addexcinfo(rawexcinfo)
def addSkip(self, testcase, reason): def addSkip(self, testcase, reason):
try: try:
pytest.skip(reason) skip(reason)
except pytest.skip.Exception: except skip.Exception:
self._evalskip = MarkEvaluator(self, 'SkipTest') self._evalskip = MarkEvaluator(self, 'SkipTest')
self._evalskip.result = True self._evalskip.result = True
self._addexcinfo(sys.exc_info()) self._addexcinfo(sys.exc_info())
def addExpectedFailure(self, testcase, rawexcinfo, reason=""): def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
try: try:
pytest.xfail(str(reason)) xfail(str(reason))
except pytest.xfail.Exception: except xfail.Exception:
self._addexcinfo(sys.exc_info()) self._addexcinfo(sys.exc_info())
def addUnexpectedSuccess(self, testcase, reason=""): def addUnexpectedSuccess(self, testcase, reason=""):
@ -152,22 +153,42 @@ def addSuccess(self, testcase):
def stopTest(self, testcase): def stopTest(self, testcase):
pass pass
def _handle_skip(self):
# implements the skipping machinery (see #2137)
# analog to pythons Lib/unittest/case.py:run
testMethod = getattr(self._testcase, self._testcase._testMethodName)
if (getattr(self._testcase.__class__, "__unittest_skip__", False) or
getattr(testMethod, "__unittest_skip__", False)):
# If the class or method was skipped.
skip_why = (getattr(self._testcase.__class__, '__unittest_skip_why__', '') or
getattr(testMethod, '__unittest_skip_why__', ''))
try: # PY3, unittest2 on PY2
self._testcase._addSkip(self, self._testcase, skip_why)
except TypeError: # PY2
if sys.version_info[0] != 2:
raise
self._testcase._addSkip(self, skip_why)
return True
return False
def runtest(self): def runtest(self):
if self.config.pluginmanager.get_plugin("pdbinvoke") is None: if self.config.pluginmanager.get_plugin("pdbinvoke") is None:
self._testcase(result=self) self._testcase(result=self)
else: else:
# disables tearDown and cleanups for post mortem debugging (see #1890) # disables tearDown and cleanups for post mortem debugging (see #1890)
if self._handle_skip():
return
self._testcase.debug() self._testcase.debug()
def _prunetraceback(self, excinfo): def _prunetraceback(self, excinfo):
pytest.Function._prunetraceback(self, excinfo) Function._prunetraceback(self, excinfo)
traceback = excinfo.traceback.filter( traceback = excinfo.traceback.filter(
lambda x:not x.frame.f_globals.get('__unittest')) lambda x: not x.frame.f_globals.get('__unittest'))
if traceback: if traceback:
excinfo.traceback = traceback excinfo.traceback = traceback
@pytest.hookimpl(tryfirst=True)
@hookimpl(tryfirst=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
if isinstance(item, TestCaseFunction): if isinstance(item, TestCaseFunction):
if item._excinfo: if item._excinfo:
@ -179,7 +200,8 @@ def pytest_runtest_makereport(item, call):
# twisted trial support # twisted trial support
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item): def pytest_runtest_protocol(item):
if isinstance(item, TestCaseFunction) and \ if isinstance(item, TestCaseFunction) and \
'twisted.trial.unittest' in sys.modules: 'twisted.trial.unittest' in sys.modules:
@ -188,7 +210,7 @@ def pytest_runtest_protocol(item):
check_testcase_implements_trial_reporter() check_testcase_implements_trial_reporter()
def excstore(self, exc_value=None, exc_type=None, exc_tb=None, def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
captureVars=None): captureVars=None):
if exc_value is None: if exc_value is None:
self._rawexcinfo = sys.exc_info() self._rawexcinfo = sys.exc_info()
else: else:
@ -197,7 +219,7 @@ def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
self._rawexcinfo = (exc_type, exc_value, exc_tb) self._rawexcinfo = (exc_type, exc_value, exc_tb)
try: try:
Failure__init__(self, exc_value, exc_type, exc_tb, Failure__init__(self, exc_value, exc_type, exc_tb,
captureVars=captureVars) captureVars=captureVars)
except TypeError: except TypeError:
Failure__init__(self, exc_value, exc_type, exc_tb) Failure__init__(self, exc_value, exc_type, exc_tb)

View File

@ -540,7 +540,7 @@ def add_hookcall_monitoring(self, before, after):
of HookImpl instances and the keyword arguments for the hook call. of HookImpl instances and the keyword arguments for the hook call.
``after(outcome, hook_name, hook_impls, kwargs)`` receives the ``after(outcome, hook_name, hook_impls, kwargs)`` receives the
same arguments as ``before`` but also a :py:class:`_CallOutcome`` object same arguments as ``before`` but also a :py:class:`_CallOutcome <_pytest.vendored_packages.pluggy._CallOutcome>` object
which represents the result of the overall hook call. which represents the result of the overall hook call.
""" """
return _TracedHookExecution(self, before, after).undo return _TracedHookExecution(self, before, after).undo

94
lib/spack/external/_pytest/warnings.py vendored Normal file
View File

@ -0,0 +1,94 @@
from __future__ import absolute_import, division, print_function
import warnings
from contextlib import contextmanager
import pytest
from _pytest import compat
def _setoption(wmod, arg):
"""
Copy of the warning._setoption function but does not escape arguments.
"""
parts = arg.split(':')
if len(parts) > 5:
raise wmod._OptionError("too many fields (max 5): %r" % (arg,))
while len(parts) < 5:
parts.append('')
action, message, category, module, lineno = [s.strip()
for s in parts]
action = wmod._getaction(action)
category = wmod._getcategory(category)
if lineno:
try:
lineno = int(lineno)
if lineno < 0:
raise ValueError
except (ValueError, OverflowError):
raise wmod._OptionError("invalid lineno %r" % (lineno,))
else:
lineno = 0
wmod.filterwarnings(action, message, category, module, lineno)
def pytest_addoption(parser):
group = parser.getgroup("pytest-warnings")
group.addoption(
'-W', '--pythonwarnings', action='append',
help="set which warnings to report, see -W option of python itself.")
parser.addini("filterwarnings", type="linelist",
help="Each line specifies a pattern for "
"warnings.filterwarnings. "
"Processed after -W and --pythonwarnings.")
@contextmanager
def catch_warnings_for_item(item):
"""
catches the warnings generated during setup/call/teardown execution
of the given item and after it is done posts them as warnings to this
item.
"""
args = item.config.getoption('pythonwarnings') or []
inifilters = item.config.getini("filterwarnings")
with warnings.catch_warnings(record=True) as log:
for arg in args:
warnings._setoption(arg)
for arg in inifilters:
_setoption(warnings, arg)
mark = item.get_marker('filterwarnings')
if mark:
for arg in mark.args:
warnings._setoption(arg)
yield
for warning in log:
warn_msg = warning.message
unicode_warning = False
if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args):
new_args = [compat.safe_str(m) for m in warn_msg.args]
unicode_warning = warn_msg.args != new_args
warn_msg.args = new_args
msg = warnings.formatwarning(
warn_msg, warning.category,
warning.filename, warning.lineno, warning.line)
item.warn("unused", msg)
if unicode_warning:
warnings.warn(
"Warning is using unicode non convertible to ascii, "
"converting to a safe representation:\n %s" % msg,
UnicodeWarning)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item):
with catch_warnings_for_item(item):
yield

View File

@ -2,19 +2,7 @@
""" """
pytest: unit and functional testing with Python. pytest: unit and functional testing with Python.
""" """
__all__ = [
'main',
'UsageError',
'cmdline',
'hookspec',
'hookimpl',
'__version__',
]
if __name__ == '__main__': # if run as a script or by 'python -m pytest'
# we trigger the below "else" condition by the following import
import pytest
raise SystemExit(pytest.main())
# else we are imported # else we are imported
@ -22,7 +10,69 @@
main, UsageError, _preloadplugins, cmdline, main, UsageError, _preloadplugins, cmdline,
hookspec, hookimpl hookspec, hookimpl
) )
from _pytest.fixtures import fixture, yield_fixture
from _pytest.assertion import register_assert_rewrite
from _pytest.freeze_support import freeze_includes
from _pytest import __version__ from _pytest import __version__
from _pytest.debugging import pytestPDB as __pytestPDB
from _pytest.recwarn import warns, deprecated_call
from _pytest.outcomes import fail, skip, importorskip, exit, xfail
from _pytest.mark import MARK_GEN as mark, param
from _pytest.main import Item, Collector, File, Session
from _pytest.fixtures import fillfixtures as _fillfuncargs
from _pytest.python import (
Module, Class, Instance, Function, Generator,
)
_preloadplugins() # to populate pytest.* namespace so help(pytest) works from _pytest.python_api import approx, raises
set_trace = __pytestPDB.set_trace
__all__ = [
'main',
'UsageError',
'cmdline',
'hookspec',
'hookimpl',
'__version__',
'register_assert_rewrite',
'freeze_includes',
'set_trace',
'warns',
'deprecated_call',
'fixture',
'yield_fixture',
'fail',
'skip',
'xfail',
'importorskip',
'exit',
'mark',
'param',
'approx',
'_fillfuncargs',
'Item',
'File',
'Collector',
'Session',
'Module',
'Class',
'Instance',
'Function',
'Generator',
'raises',
]
if __name__ == '__main__':
# if run as a script or by 'python -m pytest'
# we trigger the below "else" condition by the following import
import pytest
raise SystemExit(pytest.main())
else:
from _pytest.compat import _setup_collect_fakemodule
_preloadplugins() # to populate pytest.* namespace so help(pytest) works
_setup_collect_fakemodule()