Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"check" convenience shortcut for subtests fixture #28

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions pytest_subtests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
from collections import ChainMap
from contextlib import contextmanager
from time import monotonic

Expand Down Expand Up @@ -102,11 +103,60 @@ def subtests(request):
yield SubTests(request.node.ihook, suspend_capture_ctx, request)


@pytest.fixture
def check(subtests):
return subtests


@attr.s
class SubTestParams:
msg = attr.ib(type=str, default=None)
kwargs = attr.ib(type=dict, default=None)
parent = attr.ib(type=ChainMap, default=None)
contextmanager = attr.ib(default=None)

def context(self):
params = self.parent
if self.kwargs:
if params:
params = params.new_child(self.kwargs)
else:
params = self.kwargs
if not params:
params = {}
# xdist can not serialize ChainMap
if isinstance(params, ChainMap):
params = dict(params.items())
return SubTestContext(self.msg, params)

def child(self):
chld = SubTestParams()
if self.msg is not None:
chld.msg = self.msg
if self.parent is not None:
if self.kwargs is not None:
chld.parent = self.parent.new_child(self.kwargs)
else:
chld.parent = self.parent
elif self.kwargs is not None:
chld.parent = ChainMap(self.kwargs)
return chld

def updated(self, msg=None, **kwargs):
copy = attr.evolve(self)
if msg is not None:
copy.msg = msg
if kwargs:
copy.kwargs = kwargs.copy()
return copy


@attr.s
class SubTests(object):
ihook = attr.ib()
suspend_capture_ctx = attr.ib()
request = attr.ib()
_params = attr.ib(default=attr.Factory(SubTestParams), init=False)

@property
def item(self):
Expand Down Expand Up @@ -145,8 +195,27 @@ def _capturing_output(self):
captured.out = out
captured.err = err

@contextmanager
def test(self, msg=None, **kwargs):
"""Compatibility method, use ``subtest(msg, i=3)``"""
return self.__call__(msg, **kwargs)

@contextmanager
def _nested_scope(self, saved_params):
if saved_params is None:
saved_params = self._params
self._params = self._params.child()
try:
yield
finally:
self._params = saved_params

@contextmanager
def _subtest(self, saved_params=None):
with self._nested_scope(saved_params), self._capture_result():
yield

@contextmanager
def _capture_result(self):
start = monotonic()
exc_info = None

Expand All @@ -160,13 +229,29 @@ def test(self, msg=None, **kwargs):

call_info = CallInfo(None, exc_info, start, stop, when="call")
sub_report = SubTestReport.from_item_and_call(item=self.item, call=call_info)
sub_report.context = SubTestContext(msg, kwargs.copy())
sub_report.context = self._params.context()

captured.update_report(sub_report)

with self.suspend_capture_ctx():
self.ihook.pytest_runtest_logreport(report=sub_report)

def __call__(self, msg=None, **kwargs):
saved_params = self._params
self._params = self._params.updated(msg, **kwargs)
return self._subtest(saved_params)

def __enter__(self):
subtest = self._subtest()
retval = subtest.__enter__()
self._params.contextmanager = subtest
return retval

def __exit__(self, exc_type, exc_val, exc_tb):
subtest = self._params.contextmanager
self._params.contextmanager = None
return subtest.__exit__(exc_type, exc_val, exc_tb)


@attr.s
class Captured:
Expand Down
132 changes: 132 additions & 0 deletions tests/test_shortcut.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Tests of convenience shortcuts for the subtests fixture."""
import pytest

_cases = [
(
"concise",
"""
from datetime import datetime

def test_fields({fixturename}):
dt = datetime.utcfromtimestamp(1234567890)
with {fixturename}: assert dt.year == 2009, "OK"
with {fixturename}: assert dt.month == 1
with {fixturename}: assert dt.day == 13, "OK"
with {fixturename}: assert dt.hour == 27
""",
[
"*= FAILURES =*",
"*_ test_fields (<subtest>) _*",
"E*assert 2 == 1",
"*_ test_fields (<subtest>) _*",
"E*assert 23 == 27",
# "FAILED *::test_fields - assert 2 == 1*",
# "FAILED *::test_fields - assert 23 == 27*",
# "*= 2 failed*",
],
),
(
"steps",
"""
from datetime import datetime

def test_steps({fixturename}):
'''Document steps using {fixturename}.__call__()'''
dt = datetime.utcfromtimestamp(1234567890)
{fixturename}("date")
with {fixturename}: assert dt.year == 2009, "OK"
with {fixturename}: assert dt.month == 1
with {fixturename}: assert dt.day == 13, "OK"
{fixturename}("time")
with {fixturename}: assert dt.hour == 27
with {fixturename}: assert dt.minute == 31, "OK"
with {fixturename}: assert dt.second == 30, "OK"
""",
[
"*= FAILURES =*",
"*_ test_steps [[]date[]] _*",
"E*assert 2 == 1",
"*_ test_steps [[]time[]] _*",
"E*assert 23 == 27",
# "FAILED *::test_steps - assert 2 == 1*",
# "FAILED *::test_steps - assert 23 == 27*",
# "*= 2 failed*",
],
),
(
"test_subtest_steps",
"""
from datetime import datetime

def test_subtest_steps({fixturename}):
'''Document steps using {fixturename}.__call__()'''
dt = datetime.utcfromtimestamp(1234567890)
with {fixturename}:
{fixturename}("date")
with {fixturename}: assert dt.year == 2009, "OK"
with {fixturename}: assert dt.month == 1
with {fixturename}: assert dt.day == 13, "OK"
{fixturename}("time")
with {fixturename}: assert dt.hour == 27
with {fixturename}: assert dt.minute == 31, "OK"
with {fixturename}: assert dt.second == 30, "OK"
""",
[
"*= FAILURES =*",
"*_ test_subtest_steps [[]date[]] _*",
"E*assert 2 == 1",
"*_ test_subtest_steps [[]time[]] _*",
"E*assert 23 == 27",
# "FAILED *::test_subtest_steps - assert 2 ==*",
# "FAILED *::test_subtest_steps - assert 23 =*",
# "*= 2 failed*",
],
),
(
"nested",
"""
def test_unit_nested_args({fixturename}):
with {fixturename}('outer subtest', outer=1, to_override='out'):
with {fixturename}('inner subtest', to_override='in', inner=1):
assert 5 == 2*2, 'inner assert msg'
assert 3 == 1 + 1, 'outer assert msg'
""",
[
"*= FAILURES =*",
"*_ test_unit_nested_args [[]inner subtest[]] (inner=1, outer=1, to_override='in') _*",
"E*AssertionError: inner assert msg*",
"*_ test_unit_nested_args [[]outer subtest[]] (outer=1, to_override='out') _*",
"E*AssertionError: outer assert msg",
# "FAILED *::test_unit_nested_args - AssertionError: inner a*",
# "FAILED *::test_unit_nested_args - AssertionError: outer a*",
# "*= 2 failed*",
],
),
]


def _get_case_id(case):
return case[0]


@pytest.mark.parametrize("runner", ["normal", "xdist"])
@pytest.mark.parametrize("fixturename", ["subtests", "check"])
@pytest.mark.parametrize("case", _cases, ids=_get_case_id)
def test_subtest_sugar(testdir, case, runner, fixturename):
_id, script_template, expected_lines_template = case

testdir.makepyfile(script_template.format(fixturename=fixturename))
if runner == "normal":
result = testdir.runpytest()
collected_message = "collected 1 item"
elif runner == "xdist":
result = testdir.runpytest("-n1")
collected_message = "gw0 [1]"
else:
pytest.fail("Unsupported runner {}".format(runner))

expected_lines = [
template.format(collected_message=collected_message)
for template in ["{collected_message}"] + expected_lines_template
]
result.stdout.fnmatch_lines(expected_lines)