diff --git a/src/pytest_dependency.py b/src/pytest_dependency.py index 43224ee..af55e60 100644 --- a/src/pytest_dependency.py +++ b/src/pytest_dependency.py @@ -3,7 +3,12 @@ __version__ = "$VERSION" import logging +from pathlib import Path + +import py import pytest +from _pytest.mark import ParameterSet +from _pytest.python import Module logger = logging.getLogger(__name__) @@ -132,7 +137,9 @@ def pytest_addoption(parser): parser.addoption("--ignore-unknown-dependency", action="store_true", default=False, help="ignore dependencies whose outcome is not known") - + parser.addini("collect_dependencies", + "Collect the dependent' tests", + type="bool", default=False) def pytest_configure(config): global _automark, _ignore_unknown @@ -170,3 +177,194 @@ 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): + """Collect the dependencies of a test item and add them to the list of items. + + :param config: the pytest configuration. + :param item: the test item where to look for dependencies. + :param items: the current list of pytest items. + """ + dependency_markers = get_dependency_markers(item) + dependencies = get_dependencies_from_markers(config, item, dependency_markers) + add_dependencies(config, item, items, dependencies) + + +def get_dependency_markers(item): + """Get all the dependency markers of a test item. + + This function looks in both pytest.mark.dependency and pytest.mark.parametrize markers. + + :param item: the test item to look for dependency markers. + :return: a list of dependency markers. + """ + dependency_markers = list() + markers = item.own_markers + for marker in markers: + if marker.name == 'dependency': + dependency_markers.append(marker) + elif marker.name == 'parametrize': + append_parametrized_dependency_markers(marker, dependency_markers) + return dependency_markers + + +def append_parametrized_dependency_markers(marker, dependency_markers): + """Append dependency markers from a parametrized marker to a list. + + :param marker: the parametrize marker to look for dependency markers. + :param dependency_markers: the list to append the dependency markers. + """ + for arg in marker.args: + if isinstance(arg, list): + for param in arg: + if isinstance(param, ParameterSet): + if isinstance(param.marks, tuple): + for mark in param.marks: + if mark.name == 'dependency': + dependency_markers.append(mark) + + +def get_dependencies_from_markers(config, item, dependency_markers): + """Get the dependencies of a test item from its dependency markers. + + The dependencies are a list of tuples (depend_func, depend_nodeid, depend_parent). + + :param config: the pytest configuration. + :param item: the test item to look for dependencies. + :param dependency_markers: the dependency markers of the test item. + :return: the list of dependencies. + """ + dependencies = list() + for marker in dependency_markers: + marker_depends = marker.kwargs.get('depends') + scope = marker.kwargs.get('scope') + if marker.name == 'dependency' and marker_depends: + for depend in marker_depends: + if scope == 'session' or scope == 'package': + if '::' in depend: + 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) + else: + if item.cls: + # class cases + current_class_name = item.cls.__name__ + if "::" not in depend or "{}::".format(current_class_name) in depend: + # the first condition (depend does not contain ::) means that is a "mark.dependency name" or it is in the same class + # the second condition means that test method depends on another test method in the same class + depend_func = depend.split("::")[-1] + depend_parent = item.parent + else: + # test method depends on a test method in another class + depend_func = depend.split("::")[-1] + module = item.parent.parent + for cl in module.collect(): + if cl.cls and cl.cls.__name__ == depend.split("::")[0]: + depend_parent = cl + break + else: + depend_func = depend + depend_parent = item.parent + depend_nodeid = '{}::{}'.format(depend_parent.nodeid, depend_func) + # assert depend_nodeid == depend_nodeid2 + # class example: depend_func = test_b; depend_nodeid = test_class.py::TestClass::test_b; depend_parent = + dependencies.append((depend_func, depend_nodeid, depend_parent)) + return dependencies + + +def add_dependencies(config, item, items, dependencies): + """Add the dependencies of a test item to the list of items. + + Warning! This function "recursively" calls collect_dependencies. + + :param config: the pytest configuration. + :param item: the test item to look for dependencies (here is used just to get the parent in some cases). + :param items: the current list of tests items (where to add the dependent items). + :param dependencies: the dependencies to add. + + """ + for depend_func, depend_nodeid, depend_parent in dependencies: + # first look if depend_nodeid is already inside the list of items + # this solution use a double list with the two conventions of nodeid (with the real function name and with the name of dependency mark name) + # in the future should be better to normalize (using just one convention) the depend_nodeid before to compare + list_of_items_nodeid = [item_i.nodeid for item_i in items] # nodeid with the real function name + list_of_items_nodeid_name = get_list_of_nodeid_with_dependency_mark_name(items) # nodeid with the name of dependency mark name + full_list_of_items_nodeid = list_of_items_nodeid + list_of_items_nodeid_name + if depend_nodeid not in full_list_of_items_nodeid: + found = False + # first look if depend_func is the real name of a test function + item_to_add = get_test_function_item(depend_func, depend_parent) + if item_to_add is not None: + found = True + else: + logger.warning("collect_dependencies: the test function {}::{} does not exist".format(depend_parent, depend_func)) + # if not, look if depend_func is in the mark.dependency name + for item_j in item.parent.collect(): + if found: + logger.info("The test function {} is in the mark.dependency name".format(depend_func)) + break + for marker in item_j.own_markers: + if marker.name == 'dependency' and marker.kwargs.get('name') == depend_func: + item_to_add = item_j + found = True + break + if found: + items.insert(0, item_to_add) + # recursive look for dependencies into item_to_add + collect_dependencies(config, item_to_add, items) + return + + +def get_test_function_item(function_name, function_parent): + """Get the test function item (object) from its name and parent. + + :param function_name: the name of the test function. + :param function_parent: the parent of the test function (it could be the module or the class). + :return: the test function item. + """ + for item in function_parent.collect(): + if item.name == function_name: + return item + + +def get_list_of_nodeid_with_dependency_mark_name(items): + """Get the list of nodeid of all item in items using the dependency mark name convention. + + Example: + + class TestClassNamed(object): + @pytest.mark.dependency(name="a") + def test_a(self): + assert False + + The nodeid of test_a will be TestClassNamed::test_a, + but the nodeid using the dependency mark name convention will be TestClassNamed::a. + + :param items: the list of test items. + :return: the list of nodeid in the dependency mark name convention. + """ + list_of_nodeid = [] + for item in items: + markers = item.own_markers + for marker in markers: + if marker.name == 'dependency': + name = marker.kwargs.get('name') + if name: + node_id_split_list = item.nodeid.split("::") + node_id_split_list[-1] = name + # reconstruct the nodeid with the name of dependency mark name + nodeid = "::".join(node_id_split_list) + list_of_nodeid.append(nodeid) + return list_of_nodeid + + +def pytest_collection_modifyitems(config, items): + if config.getini('collect_dependencies'): + for item in items: + collect_dependencies(config, item, items) diff --git a/tests/test_04_automark.py b/tests/test_04_automark.py index 09fbc17..1527b23 100644 --- a/tests/test_04_automark.py +++ b/tests/test_04_automark.py @@ -66,7 +66,7 @@ def test_b(): "true_value", ["1", "yes", "y", "True", "true", "t", "on"] ) def test_set_true(ctestdir, true_value): - """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..273fe30 --- /dev/null +++ b/tests/test_05_collect_dependencies.py @@ -0,0 +1,756 @@ +"""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.fixture(scope="session") +def pytest_order_plugin(request): + return pytest.importorskip("pytest_order.plugin", reason="This test requires the pytest-order package") + + +def test_scope_session_collect_dependencies_true_single_test_run_4b(ctestdir, pytest_order_plugin): + """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 + """) + + +def test_collect_dependencies_true_single_class(ctestdir): + """A pytest.ini is present, collect_dependencies is set to true. + + One module with a single class with two tests, one of them depends on the other. + 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(test_scope_session_01=""" + import pytest + + class Tests: + @pytest.mark.dependency() + def test_b(self): + pass + + @pytest.mark.dependency(depends=["Tests::test_b"]) + def test_d(self): + pass + """) + + result = ctestdir.runpytest("--verbose", "test_scope_session_01.py::Tests::test_d") + result.assert_outcomes(passed=2, skipped=0, failed=0) + result.stdout.re_match_lines(r""" + test_scope_session_01.py::Tests::test_b PASSED + test_scope_session_01.py::Tests::test_d PASSED + """) + + +def test_collect_dependencies_true_different_classes(ctestdir): + """A pytest.ini is present, collect_dependencies is set to true. + + One module with two classes, each with a single test, one of them depends on the other. + 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(test_scope_session_01=""" + import pytest + + class Tests: + @pytest.mark.dependency() + def test_b(self): + pass + + class TestOtherTests: + @pytest.mark.dependency(depends=["Tests::test_b"]) + def test_d(self): + pass + """) + + result = ctestdir.runpytest("--verbose", "test_scope_session_01.py::TestOtherTests::test_d") + result.assert_outcomes(passed=2, skipped=0, failed=0) + result.stdout.re_match_lines(r""" + test_scope_session_01.py::Tests::test_b PASSED + test_scope_session_01.py::TestOtherTests::test_d PASSED + """) + + +def test_collect_dependencies_named(ctestdir): + """A pytest.ini is present, collect_dependencies is set to true. + + Explicitly select only a single test that depends on another one using mark.dependency name attribute. + Since collect_dependencies is set to true, the other test will be collected, and all tests will be run. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + + ctestdir.makepyfile(""" + import pytest + + @pytest.mark.dependency(name="a") + def test_a(): + pass + + @pytest.mark.dependency(name="b") + def test_b(): + assert False + + @pytest.mark.dependency(name="c", depends=["b"]) + def test_c(): + pass + + @pytest.mark.dependency(name="d", depends=["c","a"]) + def test_d(): + pass + """) + result = ctestdir.runpytest("--verbose", "test_collect_dependencies_named.py::test_d") + result.assert_outcomes(passed=1, skipped=2, failed=1) + result.stdout.re_match_lines(r""" + .*::test_a PASSED + .*::test_b FAILED + .*::test_c SKIPPED(?:\s+\(.*\))? + .*::test_d SKIPPED(?:\s+\(.*\))? + """) + + +def test_collect_simple_params(ctestdir, pytest_order_plugin): + """A pytest.ini is present, collect_dependencies is set to true. + + Explicitly select only a single test that depends on another one using mark.dependency name attribute. + Since collect_dependencies is set to true, the other test will be collected, and all tests will be run. + + Simple test for a dependency on a parametrized test. + pytest-order is required to reorder the tests in the parametrized cases. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + + ctestdir.makepyfile(""" + import pytest + + _md = pytest.mark.dependency + + @pytest.mark.parametrize("x", [ 0, 1 ]) + @pytest.mark.dependency() + def test_a(x): + assert x == 0 + + @pytest.mark.parametrize("x", [ + pytest.param(0, marks=_md(depends=["test_a[0]"])), + pytest.param(1, marks=_md(depends=["test_a[1]"])), + ]) + def test_b(x): + pass + """) + result = ctestdir.runpytest("--verbose", "test_collect_simple_params.py::test_b", "--order-dependencies") + result.assert_outcomes(passed=2, skipped=1, failed=1) + # match the following output lines without take into account the order of the tests + result.stdout.re_match_lines(r""" + .*::test_a\[0\] PASSED + """) + result.stdout.re_match_lines(r""" + .*::test_a\[1\] FAILED + """) + result.stdout.re_match_lines(r""" + .*::test_b\[0\] PASSED + """) + result.stdout.re_match_lines(r""" + .*::test_b\[1\] SKIPPED(?:\s+\(.*\))? + """) + + +def test_collect_multiple_param(ctestdir, pytest_order_plugin): + """A pytest.ini is present, collect_dependencies is set to true. + + Explicitly select only a single test that depends on another one using mark.dependency name attribute. + Since collect_dependencies is set to true, the other test will be collected, and all tests will be run. + + A scenario featuring parametrized tests. + pytest-order is required to reorder the tests in the parametrized cases. + """ + ctestdir.makefile('.ini', pytest=""" + [pytest] + collect_dependencies = true + console_output_style = classic + """) + + ctestdir.makepyfile(""" + import pytest + + _md = pytest.mark.dependency + + @pytest.mark.parametrize("x,y", [ + pytest.param(0, 0, marks=_md(name="a1")), + pytest.param(0, 1, marks=_md(name="a2")), + pytest.param(1, 0, marks=_md(name="a3")), + pytest.param(1, 1, marks=_md(name="a4")) + ]) + def test_a(x,y): + assert x==0 or y==0 + + @pytest.mark.parametrize("u,v", [ + pytest.param(1, 2, marks=_md(name="b1", depends=["a1", "a2"])), + pytest.param(1, 3, marks=_md(name="b2", depends=["a1", "a3"])), + pytest.param(1, 4, marks=_md(name="b3", depends=["a1", "a4"])), + pytest.param(2, 3, marks=_md(name="b4", depends=["a2", "a3"])), + pytest.param(2, 4, marks=_md(name="b5", depends=["a2", "a4"])), + pytest.param(3, 4, marks=_md(name="b6", depends=["a3", "a4"])) + ]) + def test_b(u,v): + pass + + @pytest.mark.parametrize("w", [ + pytest.param(1, marks=_md(name="c1", depends=["b1", "b3", "b5"])), + pytest.param(2, marks=_md(name="c2", depends=["b1", "b3", "b6"])), + pytest.param(3, marks=_md(name="c3", depends=["b1", "b2", "b4"])) + ]) + def test_c(w): + pass + """) + result = ctestdir.runpytest("--verbose", "test_collect_multiple_param.py::test_c", "--order-dependencies") + result.assert_outcomes(passed=7, skipped=5, failed=1) + # match the following output lines without take into account the order of the tests + result.stdout.re_match_lines(r""" + .*::test_a\[0-0\] PASSED + """) + result.stdout.re_match_lines(r""" + .*::test_a\[0-1\] PASSED + """) + result.stdout.re_match_lines(r""" + .*::test_a\[1-0\] PASSED + """) + result.stdout.re_match_lines(r""" + .*::test_a\[1-1\] FAILED + """) + result.stdout.re_match_lines(r""" + .*::test_b\[1-2\] PASSED + """) + result.stdout.re_match_lines(r""" + .*::test_b\[1-3\] PASSED + """) + result.stdout.re_match_lines(r""" + .*::test_b\[1-4\] SKIPPED(?:\s+\(.*\))? + """) + result.stdout.re_match_lines(r""" + .*::test_b\[2-3\] PASSED + """) + result.stdout.re_match_lines(r""" + .*::test_b\[2-4\] SKIPPED(?:\s+\(.*\))? + """) + result.stdout.re_match_lines(r""" + .*::test_b\[3-4\] SKIPPED(?:\s+\(.*\))? + """) + result.stdout.re_match_lines(r""" + .*::test_c\[1\] SKIPPED(?:\s+\(.*\))? + """) + result.stdout.re_match_lines(r""" + .*::test_c\[2\] SKIPPED(?:\s+\(.*\))? + """) + result.stdout.re_match_lines(r""" + .*::test_c\[3\] PASSED + """)