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 Jul 12, 2015
1 parent 0624a69 commit 52e0fcd
Show file tree
Hide file tree
Showing 10 changed files with 69 additions and 293 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
2.8.0.dev (compared to 2.7.X)
-----------------------------

- 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,
or no tests were run at all (this is a partial fix for issue500).
or no tests were run at all (this is a partial fix for issue500).
Thanks Eric Siegerman.

- fix issue713: JUnit XML reports for doctest failures.
Expand Down
4 changes: 4 additions & 0 deletions _pytest/hookspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,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 @@ -190,7 +190,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 @@ -200,17 +203,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 @@ -248,10 +281,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 @@ -641,51 +671,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 52e0fcd

Please sign in to comment.