pytest-dev / pytest-bdd

BDD library for the pytest runner
https://pytest-bdd.readthedocs.io/en/latest/
MIT License
1.31k stars 221 forks source link

Python 3.13: test_generate_missing fails with unbounded recursion #643

Closed musicinmybrain closed 6 months ago

musicinmybrain commented 1 year ago

Using the current master, 30ba3f7ba151768f227613eb2a186d4bc5c262aa:

$ python3.13 --version
Python 3.13.0a1
$ tox -e py3.13
[…]
====================================== FAILURES =======================================
________________________________ test_generate_missing ________________________________

pytester = <Pytester PosixPath('/tmp/pytest-of-ben/pytest-22/test_generate_missing0')>

    def test_generate_missing(pytester):
        """Test generate missing command."""
        pytester.makefile(
            ".feature",
            generation=textwrap.dedent(
                """\
                Feature: Missing code generation

                    Background:
                        Given I have a foobar

                    Scenario: Scenario tests which are already bound to the tests stay as is
                        Given I have a bar

                    Scenario: Code is generated for scenarios which are not bound to any tests
                        Given I have a bar

                    Scenario: Code is generated for scenario steps which are not yet defined(implemented)  
                        Given I have a custom bar
                """
            ),  
        )   

        pytester.makepyfile(
            textwrap.dedent(
                """\
            import functools

            from pytest_bdd import scenario, given

            scenario = functools.partial(scenario, "generation.feature")

            @given("I have a bar")
            def _():
                return "bar"

            @scenario("Scenario tests which are already bound to the tests stay as is")
            def test_foo():
                pass

            @scenario("Code is generated for scenario steps which are not yet defined(implemented)")
            def test_missing_steps():
                pass
            """
            )
        )

        result = pytester.runpytest("--generate-missing", "--feature", "generation.feature")
        result.assert_outcomes(passed=0, failed=0, errors=0)
        assert not result.stderr.str()
>       assert result.ret == 0
E       assert <ExitCode.INTERNAL_ERROR: 3> == 0
E        +  where <ExitCode.INTERNAL_ERROR: 3> = <RunResult ret=3 len(stdout.lines)=19 len(stderr.lines)=0 duration=0.04s>.ret

pytester   = <Pytester PosixPath('/tmp/pytest-of-ben/pytest-22/test_generate_missing0')>
result     = <RunResult ret=3 len(stdout.lines)=19 len(stderr.lines)=0 duration=0.04s>

/home/ben/src/forks/pytest-bdd/tests/generation/test_generate_missing.py:69: AssertionError
-------------------------------- Captured stdout call ---------------------------------
================================= test session starts =================================
platform linux -- Python 3.13.0a1, pytest-7.4.3, pluggy-1.3.0
rootdir: /tmp/pytest-of-ben/pytest-22/test_generate_missing0
plugins: bdd-7.0.0
collected 2 items
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/home/ben/src/forks/pytest-bdd/.tox/py3.13/lib/python3.13/site-packages/_pytest/main.py", line 271, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>                          ^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File "/home/ben/src/forks/pytest-bdd/.tox/py3.13/lib/python3.13/site-packages/pytest_bdd/generation.py", line 184, in _show_missing_code_main
INTERNALERROR>     if scenario in scenarios:
INTERNALERROR>        ^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File "<string>", line 4, in __eq__
INTERNALERROR>   File "<string>", line 4, in __eq__
INTERNALERROR>   File "<string>", line 4, in __eq__
INTERNALERROR>   [Previous line repeated 489 more times]
INTERNALERROR> RecursionError: maximum recursion depth exceeded in comparison

================================ no tests ran in 0.01s ================================
=============================== short test summary info ===============================
FAILED tests/generation/test_generate_missing.py::test_generate_missing - assert <ExitCode.INTERNAL_ERROR: 3> == 0
====================== 1 failed, 119 passed, 1 skipped in 9.83s =======================
py3.13: exit 1 (10.25 seconds) /home/ben/src/forks/pytest-bdd> pytest -vvl pid=1875907
.pkg: _exit> python /usr/lib/python3.11/site-packages/pyproject_api/_backend.py True poetry.core.masonry.api
  py3.13: FAIL code 1 (17.65=setup[7.40]+cmd[10.25] seconds)
  evaluation failed :( (17.71 seconds)
encukou commented 10 months ago

This is due to the following in parser.py:


@dataclass
class Feature:
    scenarios: OrderedDict[str, ScenarioTemplate]
    [...]

@dataclass
class ScenarioTemplate:
    feature: Feature
    [...]

That is, a Feature references all its ScenarioTemplates, and a ScenarioTemplate references all its Features. Both are in a dataclass field with the default compare=True, so comparing scenarios (if scenario in scenarios: in _show_missing_code_main) results in infinite recursion.

Perhaps Feature should be compared by identity rather than by its contents? If that's the case, the fix would be to use:

@dataclass(eq=False)
class Feature:
    ...
musicinmybrain commented 6 months ago

It looks like this was fixed in https://github.com/pytest-dev/pytest-bdd/pull/682.

musicinmybrain commented 6 months ago

It would be neat to get this in a release at some point to make it easier to test dependent projects with Python 3.13.

youtux commented 5 months ago

I just released pytest-bdd 7.2.0 with explicit support for python 3.13

musicinmybrain commented 5 months ago

Wonderful! Thank you.