Merge remote-tracking branch 'upstream/features' into integrate-pytest-warnings

This commit is contained in:
Bruno Oliveira 2017-03-20 19:59:05 -03:00
commit 9f85584656
77 changed files with 345 additions and 149 deletions

View File

@ -16,6 +16,9 @@ New Features
* ``pytest.raises`` now asserts that the error message matches a text or regex * ``pytest.raises`` now asserts that the error message matches a text or regex
with the ``match`` keyword argument. Thanks `@Kriechi`_ for the PR. with the ``match`` keyword argument. Thanks `@Kriechi`_ for the PR.
* ``pytest.param`` can be used to declare test parameter sets with marks and test ids.
Thanks `@RonnyPfannschmidt`_ for the PR.
Changes Changes
------- -------

View File

@ -206,12 +206,12 @@ but here is a simple overview:
#. Run all the tests #. Run all the tests
You need to have Python 2.7 and 3.5 available in your system. Now You need to have Python 2.7 and 3.6 available in your system. Now
running tests is as simple as issuing this command:: running tests is as simple as issuing this command::
$ tox -e linting,py27,py35 $ tox -e linting,py27,py36
This command will run tests via the "tox" tool against Python 2.7 and 3.5 This command will run tests via the "tox" tool against Python 2.7 and 3.6
and also perform "lint" coding-style checks. and also perform "lint" coding-style checks.
#. You can now edit your local working copy. #. You can now edit your local working copy.
@ -223,9 +223,9 @@ but here is a simple overview:
$ tox -e py27 -- --pdb $ tox -e py27 -- --pdb
Or to only run tests in a particular test module on Python 3.5:: Or to only run tests in a particular test module on Python 3.6::
$ tox -e py35 -- testing/test_config.py $ tox -e py36 -- testing/test_config.py
#. Commit and push once your tests pass and you are happy with your change(s):: #. Commit and push once your tests pass and you are happy with your change(s)::

View File

@ -57,7 +57,7 @@ If things do not work right away:
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

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,6 +2,7 @@
# 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):

View File

@ -1,3 +1,4 @@
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

View File

@ -1,4 +1,4 @@
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

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,6 +1,7 @@
""" """
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 sys import sys

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

View File

@ -4,7 +4,7 @@ Utilities for truncating assertion output.
Current default behaviour is to truncate assertion explanations at Current default behaviour is to truncate assertion explanations at
~8 terminal lines, unless running in "-vv" mode or running on CI. ~8 terminal lines, unless running in "-vv" mode or running on CI.
""" """
from __future__ import absolute_import, division, print_function
import os import os
import py import py

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

View File

@ -4,7 +4,7 @@ merged implementation of the cache provider
the name cache was not chosen 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

View File

@ -2,7 +2,7 @@
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

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

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

View File

@ -1,5 +1,5 @@
""" 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

View File

@ -5,7 +5,7 @@ that is planned to be removed in the next pytest release.
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
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.'

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

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import sys import sys
from py._code.code import FormattedExcinfo from py._code.code import FormattedExcinfo

View File

@ -2,6 +2,8 @@
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(): def pytest_namespace():
return {'freeze_includes': freeze_includes} return {'freeze_includes': freeze_includes}
@ -42,4 +44,4 @@ def _iter_all_modules(package, prefix=''):
for m in _iter_all_modules(os.path.join(path, name), prefix=name + '.'): for m in _iter_all_modules(os.path.join(path, name), prefix=name + '.'):
yield prefix + m yield prefix + m
else: else:
yield prefix + name yield prefix + name

View File

@ -1,4 +1,6 @@
""" 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 import os, sys

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

View File

@ -1,4 +1,6 @@
""" 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

View File

@ -1,20 +1,77 @@
""" 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
from collections import namedtuple from collections import namedtuple
from operator import attrgetter from operator import attrgetter
from .compat import imap from .compat import imap
def alias(name): def alias(name):
return property(attrgetter(name), doc='alias for ' + name) return property(attrgetter(name), 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,
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):
"""Error in use of a pytest marker/attribute.""" """Error in use of a pytest marker/attribute."""
def pytest_namespace(): def pytest_namespace():
return {'mark': MarkGenerator()} return {
'mark': MarkGenerator(),
'param': ParameterSet.param,
}
def pytest_addoption(parser): def pytest_addoption(parser):
@ -210,6 +267,7 @@ 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(object): class MarkDecorator(object):
""" 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
@ -255,8 +313,11 @@ class MarkDecorator(object):
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
def __repr__(self): def __repr__(self):
return "<MarkDecorator %r>" % self.mark return "<MarkDecorator %r>" % (self.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.
@ -289,19 +350,7 @@ class MarkDecorator(object):
return self.__class__(self.mark.combined_with(mark)) return self.__class__(self.mark.combined_with(mark))
def extract_argvalue(maybe_marked_args):
# TODO: incorrect mark data, the old code wanst able to collect lists
# individual parametrized argument sets can be wrapped in a series
# of markers in which case we unwrap the values and apply the mark
# at Function init
newmarks = {}
argval = maybe_marked_args
while isinstance(argval, MarkDecorator):
newmark = MarkDecorator(Mark(
argval.markname, argval.args[:-1], argval.kwargs))
newmarks[newmark.name] = newmark
argval = argval.args[-1]
return argval, newmarks
class Mark(namedtuple('Mark', 'name, args, kwargs')): class Mark(namedtuple('Mark', 'name, args, kwargs')):

View File

@ -1,4 +1,5 @@
""" monkeypatching and mocking functionality. """ """ monkeypatching and mocking functionality. """
from __future__ import absolute_import, division, print_function
import os, sys import os, sys
import re import re

View File

@ -1,4 +1,5 @@
""" run test suites written for nose. """ """ run test suites written for nose. """
from __future__ import absolute_import, division, print_function
import sys import sys

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

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
@ -12,8 +14,6 @@ from fnmatch import fnmatch
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
from py.builtin import print_
from _pytest.capture import MultiCapture, SysCapture from _pytest.capture import MultiCapture, SysCapture
from _pytest._code import Source from _pytest._code import Source
import py import py
@ -229,15 +229,15 @@ class HookRecorder(object):
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))
@ -924,8 +924,8 @@ class Testdir(object):
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:
@ -951,7 +951,7 @@ class Testdir(object):
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,))

View File

@ -1,4 +1,5 @@
""" Python test discovery, setup and run of test functions. """ """ Python test discovery, setup and run of test functions. """
from __future__ import absolute_import, division, print_function
import fnmatch import fnmatch
import inspect import inspect
@ -787,36 +788,35 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
to set a dynamic scope using test context or configuration. to set a dynamic scope using test context or configuration.
""" """
from _pytest.fixtures import scope2index from _pytest.fixtures import scope2index
from _pytest.mark import extract_argvalue from _pytest.mark import ParameterSet
from py.io import saferepr from py.io import saferepr
unwrapped_argvalues = []
newkeywords = []
for maybe_marked_args in argvalues:
argval, newmarks = extract_argvalue(maybe_marked_args)
unwrapped_argvalues.append(argval)
newkeywords.append(newmarks)
argvalues = unwrapped_argvalues
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 len(argnames) == 1: force_tuple = len(argnames) == 1
argvalues = [(val,) for val in argvalues] else:
if not argvalues: force_tuple = False
argvalues = [(NOTSET,) * len(argnames)] parameters = [
# we passed a empty list to parameterize, skip that test ParameterSet.extract_from(x, legacy_force_tuple=force_tuple)
# for x in argvalues]
del argvalues
if not parameters:
fs, lineno = getfslineno(self.function) fs, lineno = getfslineno(self.function)
newmark = pytest.mark.skip( reason = "got empty parameter set %r, function %s at %s:%d" % (
reason="got empty parameter set %r, function %s at %s:%d" % ( argnames, self.function.__name__, fs, lineno)
argnames, self.function.__name__, fs, lineno)) mark = pytest.mark.skip(reason=reason)
newkeywords = [{newmark.markname: newmark}] parameters.append(ParameterSet(
values=(NOTSET,) * len(argnames),
marks=[mark],
id=None,
))
if scope is None: if scope is None:
scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
scopenum = scope2index( scopenum = scope2index(scope, descr='call to {0}'.format(self.parametrize))
scope, descr='call to {0}'.format(self.parametrize))
valtypes = {} valtypes = {}
for arg in argnames: for arg in argnames:
if arg not in self.fixturenames: if arg not in self.fixturenames:
@ -844,22 +844,22 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
idfn = ids idfn = ids
ids = None ids = None
if ids: if ids:
if len(ids) != len(argvalues): if len(ids) != len(parameters):
raise ValueError('%d tests specified with %d ids' %( raise ValueError('%d tests specified with %d ids' % (
len(argvalues), len(ids))) len(parameters), len(ids)))
for id_value in ids: for id_value in ids:
if id_value is not None and not isinstance(id_value, py.builtin._basestring): if id_value is not None and not isinstance(id_value, py.builtin._basestring):
msg = 'ids must be list of strings, found: %s (type: %s)' msg = 'ids must be list of strings, found: %s (type: %s)'
raise ValueError(msg % (saferepr(id_value), type(id_value).__name__)) raise ValueError(msg % (saferepr(id_value), type(id_value).__name__))
ids = idmaker(argnames, argvalues, idfn, ids, self.config) ids = idmaker(argnames, parameters, idfn, ids, self.config)
newcalls = [] newcalls = []
for callspec in self._calls or [CallSpec2(self)]: for callspec in self._calls or [CallSpec2(self)]:
elements = zip(ids, argvalues, newkeywords, count()) elements = zip(ids, parameters, count())
for a_id, valset, keywords, param_index in elements: for a_id, param, param_index in elements:
assert len(valset) == len(argnames) assert len(param.values) == len(argnames)
newcallspec = callspec.copy(self) newcallspec = callspec.copy(self)
newcallspec.setmulti(valtypes, argnames, valset, a_id, newcallspec.setmulti(valtypes, argnames, param.values, a_id,
keywords, scopenum, param_index) param.deprecated_arg_dict, scopenum, param_index)
newcalls.append(newcallspec) newcalls.append(newcallspec)
self._calls = newcalls self._calls = newcalls
@ -958,17 +958,19 @@ def _idval(val, argname, idx, idfn, config=None):
return val.__name__ return val.__name__
return str(argname)+str(idx) return str(argname)+str(idx)
def _idvalset(idx, valset, argnames, idfn, ids, config=None): def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
if parameterset.id is not None:
return parameterset.id
if ids is None or (idx >= len(ids) or ids[idx] is None): if ids is None or (idx >= len(ids) or ids[idx] is None):
this_id = [_idval(val, argname, idx, idfn, config) this_id = [_idval(val, argname, idx, idfn, config)
for val, argname in zip(valset, argnames)] for val, argname in zip(parameterset.values, argnames)]
return "-".join(this_id) return "-".join(this_id)
else: else:
return _escape_strings(ids[idx]) return _escape_strings(ids[idx])
def idmaker(argnames, argvalues, idfn=None, ids=None, config=None): def idmaker(argnames, parametersets, idfn=None, ids=None, config=None):
ids = [_idvalset(valindex, valset, argnames, idfn, ids, config) ids = [_idvalset(valindex, parameterset, argnames, idfn, ids, config)
for valindex, valset in enumerate(argvalues)] for valindex, parameterset in enumerate(parametersets)]
if len(set(ids)) != len(ids): if len(set(ids)) != len(ids):
# The ids are not unique # The ids are not unique
duplicates = [testid for testid in ids if ids.count(testid) > 1] duplicates = [testid for testid in ids if ids.count(testid) > 1]

View File

@ -1,4 +1,6 @@
""" recording warnings during test function execution. """ """ recording warnings during test function execution. """
from __future__ import absolute_import, division, print_function
import inspect import inspect
import _pytest._code import _pytest._code

View File

@ -1,6 +1,7 @@
""" 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
@ -61,9 +62,9 @@ class ResultLog(object):
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,4 +1,6 @@
""" 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 sys import sys
from time import time from time import time

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,4 +1,6 @@
""" 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

View File

@ -2,8 +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.
""" """
import itertools 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

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

View File

@ -1,5 +1,5 @@
""" 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

View File

@ -55,17 +55,17 @@ them in turn::
$ pytest $ pytest
======= test session starts ======== ======= test session starts ========
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0 platform linux -- Python 3.5.2, pytest-3.0.3, py-1.4.31, pluggy-0.4.0
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collected 3 items collected 3 items
test_expectation.py ..F test_expectation.py ..F
======= FAILURES ======== ======= FAILURES ========
_______ test_eval[6*9-42] ________ _______ test_eval[6*9-42] ________
test_input = '6*9', expected = 42 test_input = '6*9', expected = 42
@pytest.mark.parametrize("test_input,expected", [ @pytest.mark.parametrize("test_input,expected", [
("3+5", 8), ("3+5", 8),
("2+4", 6), ("2+4", 6),
@ -73,9 +73,9 @@ them in turn::
]) ])
def test_eval(test_input, expected): def test_eval(test_input, expected):
> assert eval(test_input) == expected > assert eval(test_input) == expected
E AssertionError: assert 54 == 42 E assert 54 == 42
E + where 54 = eval('6*9') E + where 54 = eval('6*9')
test_expectation.py:8: AssertionError test_expectation.py:8: AssertionError
======= 1 failed, 2 passed in 0.12 seconds ======== ======= 1 failed, 2 passed in 0.12 seconds ========
@ -94,21 +94,42 @@ for example with the builtin ``mark.xfail``::
@pytest.mark.parametrize("test_input,expected", [ @pytest.mark.parametrize("test_input,expected", [
("3+5", 8), ("3+5", 8),
("2+4", 6), ("2+4", 6),
pytest.mark.xfail(("6*9", 42)), pytest.param("6*9", 42,
marks=pytest.mark.xfail),
]) ])
def test_eval(test_input, expected): def test_eval(test_input, expected):
assert eval(test_input) == expected assert eval(test_input) == expected
.. note::
prior to version 3.1 the supported mechanism for marking values
used the syntax::
import pytest
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("2+4", 6),
pytest.mark.xfail(("6*9", 42),),
])
def test_eval(test_input, expected):
assert eval(test_input) == expected
This was an initial hack to support the feature but soon was demonstrated to be incomplete,
broken for passing functions or applying multiple marks with the same name but different parameters.
The old syntax will be removed in pytest-4.0.
Let's run this:: Let's run this::
$ pytest $ pytest
======= test session starts ======== ======= test session starts ========
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0 platform linux -- Python 3.5.2, pytest-3.0.3, py-1.4.31, pluggy-0.4.0
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collected 3 items collected 3 items
test_expectation.py ..x test_expectation.py ..x
======= 2 passed, 1 xfailed in 0.12 seconds ======== ======= 2 passed, 1 xfailed in 0.12 seconds ========
The one parameter set which caused a failure previously now The one parameter set which caused a failure previously now
@ -181,15 +202,15 @@ Let's also run with a stringinput that will lead to a failing test::
F F
======= FAILURES ======== ======= FAILURES ========
_______ test_valid_string[!] ________ _______ test_valid_string[!] ________
stringinput = '!' stringinput = '!'
def test_valid_string(stringinput): def test_valid_string(stringinput):
> assert stringinput.isalpha() > assert stringinput.isalpha()
E AssertionError: assert False E assert False
E + where False = <built-in method isalpha of str object at 0xdeadbeef>() E + where False = <built-in method isalpha of str object at 0xdeadbeef>()
E + where <built-in method isalpha of str object at 0xdeadbeef> = '!'.isalpha E + where <built-in method isalpha of str object at 0xdeadbeef> = '!'.isalpha
test_strings.py:3: AssertionError test_strings.py:3: AssertionError
1 failed in 0.12 seconds 1 failed in 0.12 seconds

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import os import os
import sys import sys

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import sys import sys
import _pytest._code import _pytest._code

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import operator import operator
import _pytest import _pytest
@ -369,7 +370,7 @@ def test_codepath_Queue_example():
def test_match_succeeds(): def test_match_succeeds():
with pytest.raises(ZeroDivisionError) as excinfo: with pytest.raises(ZeroDivisionError) as excinfo:
0 / 0 0 // 0
excinfo.match(r'.*zero.*') excinfo.match(r'.*zero.*')
def test_match_raises_error(testdir): def test_match_raises_error(testdir):

View File

@ -1,6 +1,7 @@
# flake8: noqa # flake8: noqa
# disable flake check on this file because some constructs are strange # disable flake check on this file because some constructs are strange
# or redundant on purpose and can't be disable on a line-by-line basis # or redundant on purpose and can't be disable on a line-by-line basis
from __future__ import absolute_import, division, print_function
import sys import sys
import _pytest._code import _pytest._code

View File

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

View File

@ -274,6 +274,7 @@ class TestGenerator(object):
def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir):
o = testdir.makepyfile(""" o = testdir.makepyfile("""
from __future__ import print_function
def test_generative_order_of_execution(): def test_generative_order_of_execution():
import py, pytest import py, pytest
test_list = [] test_list = []
@ -283,8 +284,8 @@ class TestGenerator(object):
test_list.append(item) test_list.append(item)
def assert_order_of_execution(): def assert_order_of_execution():
py.builtin.print_('expected order', expected_list) print('expected order', expected_list)
py.builtin.print_('but got ', test_list) print('but got ', test_list)
assert test_list == expected_list assert test_list == expected_list
for i in expected_list: for i in expected_list:
@ -298,6 +299,7 @@ class TestGenerator(object):
def test_order_of_execution_generator_different_codeline(self, testdir): def test_order_of_execution_generator_different_codeline(self, testdir):
o = testdir.makepyfile(""" o = testdir.makepyfile("""
from __future__ import print_function
def test_generative_tests_different_codeline(): def test_generative_tests_different_codeline():
import py, pytest import py, pytest
test_list = [] test_list = []
@ -313,8 +315,8 @@ class TestGenerator(object):
test_list.append(0) test_list.append(0)
def assert_order_of_execution(): def assert_order_of_execution():
py.builtin.print_('expected order', expected_list) print('expected order', expected_list)
py.builtin.print_('but got ', test_list) print('but got ', test_list)
assert test_list == expected_list assert test_list == expected_list
yield list_append_0 yield list_append_0

View File

@ -207,37 +207,40 @@ class TestMetafunc(object):
@pytest.mark.issue250 @pytest.mark.issue250
def test_idmaker_autoname(self): def test_idmaker_autoname(self):
from _pytest.python import idmaker from _pytest.python import idmaker
result = idmaker(("a", "b"), [("string", 1.0), result = idmaker(("a", "b"), [pytest.param("string", 1.0),
("st-ring", 2.0)]) pytest.param("st-ring", 2.0)])
assert result == ["string-1.0", "st-ring-2.0"] assert result == ["string-1.0", "st-ring-2.0"]
result = idmaker(("a", "b"), [(object(), 1.0), result = idmaker(("a", "b"), [pytest.param(object(), 1.0),
(object(), object())]) pytest.param(object(), object())])
assert result == ["a0-1.0", "a1-b1"] assert result == ["a0-1.0", "a1-b1"]
# unicode mixing, issue250 # unicode mixing, issue250
result = idmaker((py.builtin._totext("a"), "b"), [({}, b'\xc3\xb4')]) result = idmaker(
(py.builtin._totext("a"), "b"),
[pytest.param({}, b'\xc3\xb4')])
assert result == ['a0-\\xc3\\xb4'] assert result == ['a0-\\xc3\\xb4']
def test_idmaker_with_bytes_regex(self): def test_idmaker_with_bytes_regex(self):
from _pytest.python import idmaker from _pytest.python import idmaker
result = idmaker(("a"), [(re.compile(b'foo'), 1.0)]) result = idmaker(("a"), [pytest.param(re.compile(b'foo'), 1.0)])
assert result == ["foo"] assert result == ["foo"]
def test_idmaker_native_strings(self): def test_idmaker_native_strings(self):
from _pytest.python import idmaker from _pytest.python import idmaker
totext = py.builtin._totext totext = py.builtin._totext
result = idmaker(("a", "b"), [(1.0, -1.1), result = idmaker(("a", "b"), [
(2, -202), pytest.param(1.0, -1.1),
("three", "three hundred"), pytest.param(2, -202),
(True, False), pytest.param("three", "three hundred"),
(None, None), pytest.param(True, False),
(re.compile('foo'), re.compile('bar')), pytest.param(None, None),
(str, int), pytest.param(re.compile('foo'), re.compile('bar')),
(list("six"), [66, 66]), pytest.param(str, int),
(set([7]), set("seven")), pytest.param(list("six"), [66, 66]),
(tuple("eight"), (8, -8, 8)), pytest.param(set([7]), set("seven")),
(b'\xc3\xb4', b"name"), pytest.param(tuple("eight"), (8, -8, 8)),
(b'\xc3\xb4', totext("other")), pytest.param(b'\xc3\xb4', b"name"),
pytest.param(b'\xc3\xb4', totext("other")),
]) ])
assert result == ["1.0--1.1", assert result == ["1.0--1.1",
"2--202", "2--202",
@ -257,7 +260,7 @@ class TestMetafunc(object):
from _pytest.python import idmaker from _pytest.python import idmaker
enum = pytest.importorskip("enum") enum = pytest.importorskip("enum")
e = enum.Enum("Foo", "one, two") e = enum.Enum("Foo", "one, two")
result = idmaker(("a", "b"), [(e.one, e.two)]) result = idmaker(("a", "b"), [pytest.param(e.one, e.two)])
assert result == ["Foo.one-Foo.two"] assert result == ["Foo.one-Foo.two"]
@pytest.mark.issue351 @pytest.mark.issue351
@ -268,9 +271,10 @@ class TestMetafunc(object):
if isinstance(val, Exception): if isinstance(val, Exception):
return repr(val) return repr(val)
result = idmaker(("a", "b"), [(10.0, IndexError()), result = idmaker(("a", "b"), [
(20, KeyError()), pytest.param(10.0, IndexError()),
("three", [1, 2, 3]), pytest.param(20, KeyError()),
pytest.param("three", [1, 2, 3]),
], idfn=ids) ], idfn=ids)
assert result == ["10.0-IndexError()", assert result == ["10.0-IndexError()",
"20-KeyError()", "20-KeyError()",
@ -284,9 +288,9 @@ class TestMetafunc(object):
def ids(val): def ids(val):
return 'a' return 'a'
result = idmaker(("a", "b"), [(10.0, IndexError()), result = idmaker(("a", "b"), [pytest.param(10.0, IndexError()),
(20, KeyError()), pytest.param(20, KeyError()),
("three", [1, 2, 3]), pytest.param("three", [1, 2, 3]),
], idfn=ids) ], idfn=ids)
assert result == ["a-a0", assert result == ["a-a0",
"a-a1", "a-a1",
@ -306,9 +310,10 @@ class TestMetafunc(object):
rec = WarningsRecorder() rec = WarningsRecorder()
with rec: with rec:
idmaker(("a", "b"), [(10.0, IndexError()), idmaker(("a", "b"), [
(20, KeyError()), pytest.param(10.0, IndexError()),
("three", [1, 2, 3]), pytest.param(20, KeyError()),
pytest.param("three", [1, 2, 3]),
], idfn=ids) ], idfn=ids)
assert [str(i.message) for i in rec.list] == [ assert [str(i.message) for i in rec.list] == [
@ -351,14 +356,21 @@ class TestMetafunc(object):
def test_idmaker_with_ids(self): def test_idmaker_with_ids(self):
from _pytest.python import idmaker from _pytest.python import idmaker
result = idmaker(("a", "b"), [(1, 2), result = idmaker(("a", "b"), [pytest.param(1, 2),
(3, 4)], pytest.param(3, 4)],
ids=["a", None]) ids=["a", None])
assert result == ["a", "3-4"] assert result == ["a", "3-4"]
def test_idmaker_with_paramset_id(self):
from _pytest.python import idmaker
result = idmaker(("a", "b"), [pytest.param(1, 2, id="me"),
pytest.param(3, 4, id="you")],
ids=["a", None])
assert result == ["me", "you"]
def test_idmaker_with_ids_unique_names(self): def test_idmaker_with_ids_unique_names(self):
from _pytest.python import idmaker from _pytest.python import idmaker
result = idmaker(("a"), [1,2,3,4,5], result = idmaker(("a"), map(pytest.param, [1,2,3,4,5]),
ids=["a", "a", "b", "c", "b"]) ids=["a", "a", "b", "c", "b"])
assert result == ["a0", "a1", "b0", "c", "b1"] assert result == ["a0", "a1", "b0", "c", "b1"]
@ -1438,6 +1450,31 @@ class TestMarkersWithParametrization(object):
reprec = testdir.inline_run() reprec = testdir.inline_run()
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)
@pytest.mark.parametrize('strict', [True, False])
def test_parametrize_marked_value(self, testdir, strict):
s = """
import pytest
@pytest.mark.parametrize(("n", "expected"), [
pytest.param(
2,3,
marks=pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict}),
),
pytest.param(
2,3,
marks=[pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict})],
),
])
def test_increment(n, expected):
assert n + 1 == expected
""".format(strict=strict)
testdir.makepyfile(s)
reprec = testdir.inline_run()
passed, failed = (0, 2) if strict else (2, 0)
reprec.assertoutcome(passed=passed, failed=failed)
def test_pytest_make_parametrize_id(self, testdir): def test_pytest_make_parametrize_id(self, testdir):
testdir.makeconftest(""" testdir.makeconftest("""
def pytest_make_parametrize_id(config, val): def pytest_make_parametrize_id(config, val):

View File

@ -1,4 +1,4 @@
from __future__ import with_statement from __future__ import absolute_import, division, print_function
import py, pytest import py, pytest
# test for _argcomplete but not specific for any application # test for _argcomplete but not specific for any application

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import sys import sys
import textwrap import textwrap

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import glob import glob
import os import os
import py_compile import py_compile

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import sys import sys
import _pytest import _pytest

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
# note: py.io capture tests where copied from # note: py.io capture tests where copied from
# pylib 1.4.20.dev2 (rev 13d9af95547e) # pylib 1.4.20.dev2 (rev 13d9af95547e)
from __future__ import with_statement from __future__ import with_statement
@ -13,7 +14,7 @@ import contextlib
from _pytest import capture from _pytest import capture
from _pytest.capture import CaptureManager from _pytest.capture import CaptureManager
from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_NOTESTSCOLLECTED
from py.builtin import print_
needsosdup = pytest.mark.xfail("not hasattr(os, 'dup')") needsosdup = pytest.mark.xfail("not hasattr(os, 'dup')")
@ -711,7 +712,7 @@ def test_dupfile(tmpfile):
assert nf != tmpfile assert nf != tmpfile
assert nf.fileno() != tmpfile.fileno() assert nf.fileno() != tmpfile.fileno()
assert nf not in flist assert nf not in flist
print_(i, end="", file=nf) print(i, end="", file=nf)
flist.append(nf) flist.append(nf)
for i in range(5): for i in range(5):
f = flist[i] f = flist[i]
@ -785,7 +786,7 @@ class TestFDCapture(object):
def test_stderr(self): def test_stderr(self):
cap = capture.FDCapture(2) cap = capture.FDCapture(2)
cap.start() cap.start()
print_("hello", file=sys.stderr) print("hello", file=sys.stderr)
s = cap.snap() s = cap.snap()
cap.done() cap.done()
assert s == "hello\n" assert s == "hello\n"

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import pytest, py import pytest, py
from _pytest.main import Session, EXIT_NOTESTSCOLLECTED from _pytest.main import Session, EXIT_NOTESTSCOLLECTED

View File

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

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import py, pytest import py, pytest
import _pytest._code import _pytest._code

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
from textwrap import dedent from textwrap import dedent
import _pytest._code import _pytest._code

View File

@ -1,4 +1,5 @@
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, division, print_function
import sys import sys
import _pytest._code import _pytest._code
from _pytest.compat import MODULE_NOT_FOUND_ERROR from _pytest.compat import MODULE_NOT_FOUND_ERROR

View File

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

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_NOTESTSCOLLECTED
import pytest import pytest

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
from xml.dom import minidom from xml.dom import minidom
import py import py
import sys import sys

View File

@ -1,7 +1,9 @@
from __future__ import absolute_import, division, print_function
import os import os
import sys
import py, pytest import pytest
from _pytest.mark import MarkGenerator as Mark from _pytest.mark import MarkGenerator as Mark, ParameterSet
class TestMark(object): class TestMark(object):
def test_markinfo_repr(self): def test_markinfo_repr(self):
@ -9,9 +11,11 @@ class TestMark(object):
m = MarkInfo(Mark("hello", (1,2), {})) m = MarkInfo(Mark("hello", (1,2), {}))
repr(m) repr(m)
def test_pytest_exists_in_namespace_all(self): @pytest.mark.parametrize('attr', ['mark', 'param'])
assert 'mark' in py.test.__all__ @pytest.mark.parametrize('modulename', ['py.test', 'pytest'])
assert 'mark' in pytest.__all__ def test_pytest_exists_in_namespace_all(self, attr, modulename):
module = sys.modules[modulename]
assert attr in module.__all__
def test_pytest_mark_notcallable(self): def test_pytest_mark_notcallable(self):
mark = Mark() mark = Mark()
@ -414,7 +418,7 @@ class TestFunctional(object):
""") """)
items, rec = testdir.inline_genitems(p) items, rec = testdir.inline_genitems(p)
for item in items: for item in items:
print (item, item.keywords) print(item, item.keywords)
assert 'a' in item.keywords assert 'a' in item.keywords
def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir): def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir):
@ -673,7 +677,7 @@ class TestKeywordSelection(object):
item.extra_keyword_matches.add("xxx") item.extra_keyword_matches.add("xxx")
""") """)
reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword) reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword)
py.builtin.print_("keyword", repr(keyword)) print("keyword", repr(keyword))
passed, skipped, failed = reprec.listoutcomes() passed, skipped, failed = reprec.listoutcomes()
assert len(passed) == 1 assert len(passed) == 1
assert passed[0].nodeid.endswith("test_2") assert passed[0].nodeid.endswith("test_2")
@ -738,3 +742,16 @@ class TestKeywordSelection(object):
assert_test_is_not_selected("__") assert_test_is_not_selected("__")
assert_test_is_not_selected("()") assert_test_is_not_selected("()")
@pytest.mark.parametrize('argval, expected', [
(pytest.mark.skip()((1, 2)),
ParameterSet(values=(1, 2), marks=[pytest.mark.skip], id=None)),
(pytest.mark.xfail(pytest.mark.skip()((1, 2))),
ParameterSet(values=(1, 2),
marks=[pytest.mark.xfail, pytest.mark.skip], id=None)),
])
def test_parameterset_extractfrom(argval, expected):
extracted = ParameterSet.extract_from(argval)
assert extracted == expected

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import os import os
import sys import sys
import textwrap import textwrap

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import pytest import pytest
def setup_module(mod): def setup_module(mod):

View File

@ -1,4 +1,4 @@
from __future__ import with_statement from __future__ import absolute_import, division, print_function
import sys import sys
import os import os
import py, pytest import py, pytest

View File

@ -1,4 +1,5 @@
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, division, print_function
import sys import sys
import pytest import pytest

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import sys import sys
import platform import platform

View File

@ -1,4 +1,5 @@
# encoding: UTF-8 # encoding: UTF-8
from __future__ import absolute_import, division, print_function
import pytest import pytest
import py import py
import os import os

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import pytest import pytest
import os import os
from _pytest.pytester import HookRecorder from _pytest.pytester import HookRecorder

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import warnings import warnings
import re import re
import py import py

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import os import os
import _pytest._code import _pytest._code

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement from __future__ import absolute_import, division, print_function
import _pytest._code import _pytest._code
import os import os

View File

@ -1,6 +1,8 @@
# """
# test correct setup/teardowns at test correct setup/teardowns at
# module, class, and instance level module, class, and instance level
"""
from __future__ import absolute_import, division, print_function
import pytest import pytest

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
import pytest import pytest
from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_NOTESTSCOLLECTED

View File

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

View File

@ -1,6 +1,7 @@
""" """
terminal reporting of the full testing process. terminal reporting of the full testing process.
""" """
from __future__ import absolute_import, division, print_function
import collections import collections
import sys import sys

View File

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

View File

@ -1,3 +1,4 @@
from __future__ import absolute_import, division, print_function
from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_NOTESTSCOLLECTED
import pytest import pytest
import gc import gc