Skip to content

Commit

Permalink
stop running yield test setupstate at collection time, fixes #16
Browse files Browse the repository at this point in the history
however in order to do so yield tests are currently less detailed
but as bonus they now support fixtures like normal tests

the Generator object was completely removed and Function was aliased to Generatot
  • Loading branch information
RonnyPfannschmidt committed Sep 27, 2015
1 parent cb4b5bd commit 2f97254
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 292 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@
users diagnose problems such as unexpected ini files which add
unknown options being picked up by pytest. Thanks to Pavel Savchenko for
bringing the problem to attention in #821 and Bruno Oliveira for the PR.
- introduce the hook pytest_pyfunc_interpret_result
this eases interpreting test functions results like generators,
twisted inline defereds, futures and asyncio generators

- replace the Generator concept with the pytest_pyfunc_interpret_result hook
(Note: this change reduces reporting detail for generator tests,
that will be addressed in a later release)

the Generator class is now a alias to Function

- Summary bar now is colored yellow for warning
situations such as: all tests either were skipped or xpass/xfailed,
Expand Down
4 changes: 4 additions & 0 deletions _pytest/hookspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ def pytest_pycollect_makeitem(collector, name, obj):
def pytest_pyfunc_call(pyfuncitem):
""" call underlying test function. """

@hookspec(firstresult=True)
def pytest_pyfunc_interpret_result(pyfuncitem, result):
""" interpret the return value of the underlying test function. """

def pytest_generate_tests(metafunc):
""" generate (multiple) parametrized calls to a test function."""

Expand Down
12 changes: 0 additions & 12 deletions _pytest/nose.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,6 @@ def pytest_runtest_makereport(item, call):
@pytest.hookimpl(trylast=True)
def pytest_runtest_setup(item):
if is_potential_nosetest(item):
if isinstance(item.parent, pytest.Generator):
gen = item.parent
if not hasattr(gen, '_nosegensetup'):
call_optional(gen.obj, 'setup')
if isinstance(gen.parent, pytest.Instance):
call_optional(gen.parent.obj, 'setup')
gen._nosegensetup = True
if not call_optional(item.obj, 'setup'):
# call module level setup if there is no object level one
call_optional(item.parent.obj, 'setup')
Expand All @@ -49,11 +42,6 @@ def teardown_nose(item):
# del item.parent._nosegensetup


def pytest_make_collect_report(collector):
if isinstance(collector, pytest.Generator):
call_optional(collector.obj, 'setup')


def is_potential_nosetest(item):
# extra check needed since we do not do nose style setup/teardown
# on direct unittest style classes
Expand Down
94 changes: 43 additions & 51 deletions _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@ def pytest_namespace():
'raises' : raises,
'collect': {
'Module': Module, 'Class': Class, 'Instance': Instance,
'Function': Function, 'Generator': Generator,
'Function': Function,
# TODO: backward compatibility check
# TODO: deprecate
'Generator': Function,
'_fillfuncargs': fillfixtures}
}

Expand All @@ -254,17 +257,47 @@ def pytestconfig(request):
return request.config


def _get_check_nameargs(obj, idx):
if not isinstance(obj, (list, tuple)):
obj = (obj,)
# explict naming

if isinstance(obj[0], py.builtin._basestring):
name = '[%s]' % obj[0]
obj = obj[1:]
else:
name = '[%d]' % idx
call, args = obj[0], obj[1:]
if not callable(call):
pytest.fail('not a check\n'
'name=%s call=%r args=%r' % (name, call, args))
return name, call, args


@pytest.hookimpl(trylast=True)
def pytest_pyfunc_interpret_result(pyfuncitem, result):
if inspect.isgenerator(result):
pyfuncitem.warn(
code='G01',
message='generator test, reporting is limited')
for idx, check in enumerate(result):
name, call, args = _get_check_nameargs(check, idx)
# TODO: more detailed logging
# TODO: check level runtest_protocol
call(*args)

@pytest.hookimpl(trylast=True)
def pytest_pyfunc_call(pyfuncitem):
testfunction = pyfuncitem.obj
if pyfuncitem._isyieldedfunction():
testfunction(*pyfuncitem._args)
else:
funcargs = pyfuncitem.funcargs
testargs = {}
for arg in pyfuncitem._fixtureinfo.argnames:
testargs[arg] = funcargs[arg]
testfunction(**testargs)

funcargs = pyfuncitem.funcargs
testargs = {}
for arg in pyfuncitem._fixtureinfo.argnames:
testargs[arg] = funcargs[arg]
result = testfunction(**testargs)
pyfuncitem.ihook.pytest_pyfunc_interpret_result(
pyfuncitem=pyfuncitem,
result=result)
return True

def pytest_collect_file(path, parent):
Expand Down Expand Up @@ -301,10 +334,7 @@ def pytest_pycollect_makeitem(collector, name, obj):
"cannot collect %r because it is not a function."
% name, )
if getattr(obj, "__test__", True):
if is_generator(obj):
res = Generator(name, parent=collector)
else:
res = list(collector._genfunctions(name, obj))
res = list(collector._genfunctions(name, obj))
outcome.force_result(res)

def is_generator(func):
Expand Down Expand Up @@ -725,51 +755,13 @@ def repr_failure(self, excinfo, outerr=None):
return self._repr_failure_py(excinfo, style=style)


class Generator(FunctionMixin, PyCollector):
def collect(self):
# test generators are seen as collectors but they also
# invoke setup/teardown on popular request
# (induced by the common "test_*" naming shared with normal tests)
self.session._setupstate.prepare(self)
# see FunctionMixin.setup and test_setupstate_is_preserved_134
self._preservedparent = self.parent.obj
l = []
seen = {}
for i, x in enumerate(self.obj()):
name, call, args = self.getcallargs(x)
if not callable(call):
raise TypeError("%r yielded non callable test %r" %(self.obj, call,))
if name is None:
name = "[%d]" % i
else:
name = "['%s']" % name
if name in seen:
raise ValueError("%r generated tests with non-unique name %r" %(self, name))
seen[name] = True
l.append(self.Function(name, self, args=args, callobj=call))
return l

def getcallargs(self, obj):
if not isinstance(obj, (tuple, list)):
obj = (obj,)
# explict naming
if isinstance(obj[0], py.builtin._basestring):
name = obj[0]
obj = obj[1:]
else:
name = None
call, args = obj[0], obj[1:]
return name, call, args


def hasinit(obj):
init = getattr(obj, '__init__', None)
if init:
if init != object.__init__:
return True



def fillfixtures(function):
""" fill missing funcargs for a test function. """
try:
Expand Down
Loading

0 comments on commit 2f97254

Please sign in to comment.