diff --git a/pytest_dependency.py b/pytest_dependency.py index 9ebf94a..3125d0e 100644 --- a/pytest_dependency.py +++ b/pytest_dependency.py @@ -3,7 +3,11 @@ __version__ = "$VERSION" import logging +from pathlib import Path + +import py import pytest +from _pytest.python import Module logger = logging.getLogger(__name__) @@ -32,7 +36,7 @@ class DependencyItemStatus(object): Phases = ('setup', 'call', 'teardown') def __init__(self): - self.results = { w:None for w in self.Phases } + self.results = {w: None for w in self.Phases} def __str__(self): l = ["%s: %s" % (w, self.results[w]) for w in self.Phases] @@ -140,11 +144,14 @@ def depends(request, other, scope='module'): def pytest_addoption(parser): - parser.addini("automark_dependency", - "Add the dependency marker to all tests automatically", + parser.addini("automark_dependency", + "Add the dependency marker to all tests automatically", + default=False) + parser.addini("collect_dependencies", + "Collect the dependent' tests", default=False) - parser.addoption("--ignore-unknown-dependency", - action="store_true", default=False, + parser.addoption("--ignore-unknown-dependency", + action="store_true", default=False, help="ignore dependencies whose outcome is not known") @@ -152,7 +159,7 @@ def pytest_configure(config): global _automark, _ignore_unknown _automark = _get_bool(config.getini("automark_dependency")) _ignore_unknown = config.getoption("--ignore-unknown-dependency") - config.addinivalue_line("markers", + config.addinivalue_line("markers", "dependency(name=None, depends=[]): " "mark a test to be used as a dependency for " "other tests or to depend on other tests.") @@ -184,3 +191,39 @@ def pytest_runtest_setup(item): scope = marker.kwargs.get('scope', 'module') manager = DependencyManager.getManager(item, scope=scope) manager.checkDepend(depends, item) + + +def collect_dependencies(config, item, items): + dependencies = list() + markers = item.own_markers + for marker in markers: + depends = marker.kwargs.get('depends') + scope = marker.kwargs.get('scope') + if marker.name == 'dependency' and depends: + for depend in depends: + if scope == 'session' or scope == 'package': + depend_module, depend_func = depend.split("::", 1) + depend_path = py.path.local(Path(config.rootdir) / Path(depend_module)) + depend_parent = Module.from_parent(item.parent, fspath=depend_path) + depend_nodeid = depend + else: + depend_func = depend + depend_parent = item.parent + depend_nodeid = '{}::{}'.format(depend_parent.nodeid, depend_func) + # assert depend_nodeid == depend_nodeid2 + dependencies.append((depend_func, depend_nodeid, depend_parent)) + + for depend_func, depend_nodeid, depend_parent in dependencies: + list_of_items_nodeid = [item_i.nodeid for item_i in items] + if depend_nodeid not in list_of_items_nodeid: + item_to_add = pytest.Function.from_parent(name=depend_func, parent=depend_parent) + items.insert(0, item_to_add) + # recursive look for dependencies into item_to_add + collect_dependencies(config, item_to_add, items) + return + + +def pytest_collection_modifyitems(config, items): + if _get_bool(config.getini('collect_dependencies')): + for item in items: + collect_dependencies(config, item, items) diff --git a/setup.py b/setup.py index 46c02e6..1ff05a6 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ version = f.read() except (OSError, IOError): distutils.log.warn("warning: cannot determine version number") - version = "UNKNOWN" + version = "0.5.1.post20212102+ftesser-collect_dependencies-20211216" doc_string = __doc__ diff --git a/tests/test_04_automark.py b/tests/test_04_automark.py index 7251405..edb2129 100644 --- a/tests/test_04_automark.py +++ b/tests/test_04_automark.py @@ -60,7 +60,7 @@ def test_b(): def test_set_true(ctestdir): - """A pytest.ini is present, automark_dependency is set to false. + """A pytest.ini is present, automark_dependency is set to true. Since automark_dependency is set to true, the outcome of test_a will be recorded, even though it is not marked. As a result, diff --git a/tests/test_05_collect_dependencies.py b/tests/test_05_collect_dependencies.py new file mode 100644 index 0000000..f422ba0 --- /dev/null +++ b/tests/test_05_collect_dependencies.py @@ -0,0 +1,503 @@ +"""Test the collect_dependencies option. +""" + +import pytest + + +def test_no_set_collect_dependencies(ctestdir): + """No pytest.ini file, e.g. collect_dependencies is not set. + + Explicitly select only a single test that depends on another one. + Since collect_dependencies defaults to false, and the other test has not been run at all, the selected test + will be skipped. + + """ + ctestdir.makepyfile(""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + @pytest.mark.dependency() + def test_b(): + pass + + @pytest.mark.dependency() + def test_c(): + pass + + @pytest.mark.dependency(depends=["test_c"]) + def test_d(): + pass + """) + result = ctestdir.runpytest("--verbose", "test_no_set_collect_dependencies.py::test_d") + result.assert_outcomes(passed=0, skipped=1, failed=0) + result.stdout.re_match_lines(r""" + .*::test_d SKIPPED(?:\s+\(.*\))? + """) + + +def test_collect_dependencies_false(ctestdir): + """A pytest.ini is present, collect_dependencies is set to false. + + Explicitly select only a single test that depends on another one. + Since collect_dependencies is set to false, and the other test has not been run at all, the selected test + will be skipped. + + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = false + console_output_style = classic + """) + + ctestdir.makepyfile(""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + @pytest.mark.dependency() + def test_b(): + pass + + @pytest.mark.dependency() + def test_c(): + pass + + @pytest.mark.dependency(depends=["test_c"]) + def test_d(): + pass + """) + result = ctestdir.runpytest("--verbose", "test_collect_dependencies_false.py::test_d") + result.assert_outcomes(passed=0, skipped=1, failed=0) + result.stdout.re_match_lines(r""" + .*::test_d SKIPPED(?:\s+\(.*\))? + """) + + +def test_collect_dependencies_true(ctestdir): + """A pytest.ini is present, collect_dependencies is set to true. + + Explicitly select only a single test that depends on another one. + Since collect_dependencies is set to true, the other test will be collected, and both tests will be run. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + + ctestdir.makepyfile(""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + @pytest.mark.dependency() + def test_b(): + pass + + @pytest.mark.dependency() + def test_c(): + pass + + @pytest.mark.dependency(depends=["test_c"]) + def test_d(): + pass + """) + result = ctestdir.runpytest("--verbose", "test_collect_dependencies_true.py::test_d") + result.assert_outcomes(passed=2, skipped=0, failed=0) + result.stdout.re_match_lines(r""" + .*::test_c PASSED + .*::test_d PASSED + """) + + +def test_collect_dependencies_true_recursive(ctestdir): + """A pytest.ini is present, collect_dependencies is set to true. + + Explicitly select only a single test that depends on another one, that depends from others two. + Since collect_dependencies is set to true, the dependent tests will be recursively collected, and four tests will be run. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + + ctestdir.makepyfile(""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + @pytest.mark.dependency() + def test_b(): + pass + + @pytest.mark.dependency(depends=["test_b", "test_a"]) + def test_c(): + pass + + @pytest.mark.dependency(depends=["test_c"]) + def test_d(): + pass + """) + result = ctestdir.runpytest("--verbose", "test_collect_dependencies_true_recursive.py::test_d") + result.assert_outcomes(passed=4, skipped=0, failed=0) + result.stdout.re_match_lines(r""" + .*::test_a PASSED + .*::test_b PASSED + .*::test_c PASSED + .*::test_d PASSED + """) + + +def test_scope_session_collect_dependencies_true(ctestdir): + """Two modules, some cross module dependencies in session scope. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + ctestdir.makepyfile(test_scope_session_01=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + @pytest.mark.dependency() + def test_b(): + assert False + + @pytest.mark.dependency(depends=["test_a"]) + def test_c(): + pass + + class TestClass(object): + + @pytest.mark.dependency() + def test_b(self): + pass + """, test_scope_session_02=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + assert False + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::test_a", + "test_scope_session_01.py::test_c"], + scope='session' + ) + def test_e(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::test_b"], + scope='session' + ) + def test_f(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_02.py::test_e"], + scope='session' + ) + def test_g(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::TestClass::test_b"], + scope='session' + ) + def test_h(): + pass + """) + result = ctestdir.runpytest("--verbose") + result.assert_outcomes(passed=6, skipped=1, failed=2) + result.stdout.re_match_lines(r""" + test_scope_session_01.py::test_a PASSED + test_scope_session_01.py::test_b FAILED + test_scope_session_01.py::test_c PASSED + test_scope_session_01.py::TestClass::test_b PASSED + test_scope_session_02.py::test_a FAILED + test_scope_session_02.py::test_e PASSED + test_scope_session_02.py::test_f SKIPPED(?:\s+\(.*\))? + test_scope_session_02.py::test_g PASSED + test_scope_session_02.py::test_h PASSED + """) + + +def test_scope_session_collect_dependencies_true_single_test_run_1(ctestdir): + """Two modules, some cross module dependencies in session scope. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + ctestdir.makepyfile(test_scope_session_01=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + """, test_scope_session_02=""" + import pytest + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::test_a"], + scope='session' + ) + def test_b(): + pass + + """) + + result = ctestdir.runpytest("--verbose", "test_scope_session_02.py::test_b") + result.assert_outcomes(passed=2, skipped=0, failed=0) + result.stdout.re_match_lines(r""" + test_scope_session_01.py::test_a PASSED + test_scope_session_02.py::test_b PASSED + """) + + +def test_scope_session_collect_dependencies_true_single_test_run_2(ctestdir): + """Two modules, some cross module dependencies in session scope. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + ctestdir.makepyfile(test_scope_session_01=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + """, test_scope_session_02=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::test_a", "test_scope_session_02.py::test_a"], + scope='session' + ) + def test_b(): + pass + + """) + + result = ctestdir.runpytest("--verbose", "test_scope_session_02.py::test_b") + result.assert_outcomes(passed=3, skipped=0, failed=0) + result.stdout.re_match_lines(r""" + test_scope_session_02.py::test_a PASSED + test_scope_session_01.py::test_a PASSED + test_scope_session_02.py::test_b PASSED + """) + + +def test_scope_session_collect_dependencies_true_single_test_run_3(ctestdir): + """Two modules, some cross module dependencies in session scope. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + ctestdir.makepyfile(test_scope_session_01=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + @pytest.mark.dependency(depends=["test_a"]) + def test_b(): + pass + + """, test_scope_session_02=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::test_b", "test_scope_session_02.py::test_a"], + scope='session' + ) + def test_b(): + pass + + """) + + result = ctestdir.runpytest("--verbose", "test_scope_session_02.py::test_b") + result.assert_outcomes(passed=4, skipped=0, failed=0) + result.stdout.re_match_lines(r""" + test_scope_session_02.py::test_a PASSED + test_scope_session_01.py::test_a PASSED + test_scope_session_01.py::test_b PASSED + test_scope_session_02.py::test_b PASSED + """) + + +def test_scope_session_collect_dependencies_true_single_test_run_4a(ctestdir): + """Two modules, some cross module dependencies in session scope. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + ctestdir.makepyfile(test_scope_session_01=""" + import pytest + + @pytest.mark.dependency(depends=["test_c"]) + def test_a(): + pass + + @pytest.mark.dependency() + def test_b(): + assert False + + @pytest.mark.dependency() + def test_c(): + pass + + class TestClass(object): + + @pytest.mark.dependency() + def test_b(self): + pass + """, test_scope_session_02=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + assert False + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::test_a", + "test_scope_session_01.py::test_c"], + scope='session' + ) + def test_e(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::test_b"], + scope='session' + ) + def test_f(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_02.py::test_e"], + scope='session' + ) + def test_g(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::TestClass::test_b"], + scope='session' + ) + def test_h(): + pass + """) + result = ctestdir.runpytest("--verbose", "test_scope_session_02.py::test_e") + result.assert_outcomes(passed=3, skipped=0, failed=0) + result.stdout.re_match_lines(r""" + test_scope_session_01.py::test_c PASSED + test_scope_session_01.py::test_a PASSED + test_scope_session_02.py::test_e PASSED + """) + + +@pytest.mark.skip(reason="This test requires the pytest-order package") +def test_scope_session_collect_dependencies_true_single_test_run_4b(ctestdir): + """Two modules, some cross module dependencies in session scope. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + ctestdir.makepyfile(test_scope_session_01=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + pass + + @pytest.mark.dependency() + def test_b(): + assert False + + @pytest.mark.dependency(depends=["test_scope_session_01.py::test_a"], scope='session') + def test_c(): + pass + + class TestClass(object): + + @pytest.mark.dependency() + def test_b(self): + pass + """, test_scope_session_02=""" + import pytest + + @pytest.mark.dependency() + def test_a(): + assert False + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::test_a", + "test_scope_session_01.py::test_c"], + scope='session' + ) + def test_e(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::test_b"], + scope='session' + ) + def test_f(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_02.py::test_e"], + scope='session' + ) + def test_g(): + pass + + @pytest.mark.dependency( + depends=["test_scope_session_01.py::TestClass::test_b"], + scope='session' + ) + def test_h(): + pass + """) + result = ctestdir.runpytest("--verbose", "test_scope_session_02.py::test_e", "--order-dependencies") + result.assert_outcomes(passed=3, skipped=0, failed=0) + result.stdout.re_match_lines(r""" + test_scope_session_01.py::test_a PASSED + test_scope_session_01.py::test_c PASSED + test_scope_session_02.py::test_e PASSED + """)