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

TestResult.stopTest called when test is skipped, despite TestResult.startTest not being called #113267

Closed
yvan-the-barbarian opened this issue Dec 19, 2023 · 3 comments
Labels
3.12 bugs and security fixes 3.13 bugs and security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@yvan-the-barbarian
Copy link

yvan-the-barbarian commented Dec 19, 2023

Bug report

Bug description:

Following commit 551aa6ab9419109a80ad53900ad930e9b7f2e40d a bug seems to have been introduced in the TestCase.run method.

Because the result.startTest(self) call was moved inside the try/finally block after the check to see if the test is skipped, the result.stopTest(self) call will be made even if the test is skipped and startTest is never called.

While this does not cause issues with the standard TestResult or TextTestResult classes, it can cause issues with other classes which subclass those and expect every call to stopTest to be preceded by a call from startTest.

Most notably for me, the unittest-xml-reporting package, whose stopTest method uses data initialized in startTest.

Note: Further thinking about this problem, it seems like changing the flow of methods like this was ill thought solution to the problem in the first place, as this might be an unexpected change of behaviour for different libraries. I'm still leaving what I think could be the solution in case I'm wrong on that.

It looks like the fix would seem to be moving the call to stopTest in yet another try/finally block right after the call to startTest, emulating the same behaviour as previous code while keeping the call to startTest after the skip check. The call to stopTestRun would need to remain where it is as startTestRun has already been called at this point.

So using the code as found in that commit, something like:

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
            startTestRun = getattr(result, 'startTestRun', None)
            stopTestRun = getattr(result, 'stopTestRun', None)
            if startTestRun is not None:
                startTestRun()
        else:
            stopTestRun = None

        try:
            testMethod = getattr(self, self._testMethodName)
            if (getattr(self.__class__, "__unittest_skip__", False) or
                getattr(testMethod, "__unittest_skip__", False)):
                # If the class or method was skipped.
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                _addSkip(result, self, skip_why)
                return result

            # Increase the number of tests only if it hasn't been skipped
            result.startTest(self)
            try:
                expecting_failure = (
                    getattr(self, "__unittest_expecting_failure__", False) or
                    getattr(testMethod, "__unittest_expecting_failure__", False)
                )
                outcome = _Outcome(result)
                start_time = time.perf_counter()
                try:
                    self._outcome = outcome

                    with outcome.testPartExecutor(self):
                        self._callSetUp()
                    if outcome.success:
                        outcome.expecting_failure = expecting_failure
                        with outcome.testPartExecutor(self):
                            self._callTestMethod(testMethod)
                        outcome.expecting_failure = False
                        with outcome.testPartExecutor(self):
                            self._callTearDown()
                    self.doCleanups()
                    self._addDuration(result, (time.perf_counter() - start_time))

                    if outcome.success:
                        if expecting_failure:
                            if outcome.expectedFailure:
                                self._addExpectedFailure(result, outcome.expectedFailure)
                            else:
                                self._addUnexpectedSuccess(result)
                        else:
                            result.addSuccess(self)
                    return result
                finally:
                    # explicitly break reference cycle:
                    # outcome.expectedFailure -> frame -> outcome -> outcome.expectedFailure
                    outcome.expectedFailure = None
                    outcome = None

                    # clear the outcome, no more needed
                    self._outcome = None
                    
            finally:
                result.stopTest(self)
            
        finally:
            if stopTestRun is not None:
                stopTestRun()

CPython versions tested on:

3.12

Operating systems tested on:

Linux, Windows

Linked PRs

@yvan-the-barbarian yvan-the-barbarian added the type-bug An unexpected behavior, bug, or error label Dec 19, 2023
@AlexWaygood AlexWaygood added the stdlib Python modules in the Lib dir label Dec 19, 2023
@yvan-the-barbarian yvan-the-barbarian changed the title TestResult.stopTest called when test is skipped, despite TestResult.startTest TestResult.stopTest called when test is skipped, despite TestResult.startTest not being called Dec 19, 2023
@yvan-the-barbarian
Copy link
Author

I am now also seeing failures in PyCharm's unit testing because of this issue.

This is starting to be a huge issue.

@serhiy-storchaka
Copy link
Member

I think that it was a wrong change, and not only because it makes startTest() and stopTest() calls unbalanced, but because it did not handle all cases and changed the behavior in wrong direction. #113661 was the proper fix of the original problem.

@serhiy-storchaka serhiy-storchaka added the 3.13 bugs and security fixes label Jan 23, 2024
@Muhammadamjadm Muhammadamjadm mentioned this issue Jan 27, 2024
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Feb 4, 2024
…in Python 3.12 (pythonGH-106588)" (pythonGH-114470)

This reverts commit 8fc0713.
(cherry picked from commit ecabff9)

Co-authored-by: Serhiy Storchaka <[email protected]>
serhiy-storchaka added a commit that referenced this issue Feb 4, 2024
…thon 3.12 (GH-106588)" (GH-114470) (GH-114994)

This reverts commit 8fc0713.
(cherry picked from commit ecabff9)

Co-authored-by: Serhiy Storchaka <[email protected]>
@sodul
Copy link

sodul commented Feb 7, 2024

Thanks, the changes for 3.12.2 seem to fix CleanCut/green#277

aisk pushed a commit to aisk/cpython that referenced this issue Feb 11, 2024
fsc-eriker pushed a commit to fsc-eriker/cpython that referenced this issue Feb 14, 2024
wesleybl added a commit to plone/plone.restapi that referenced this issue Feb 27, 2024
davisagli pushed a commit to plone/plone.restapi that referenced this issue Feb 28, 2024
mister-roboto pushed a commit to plone/buildout.coredev that referenced this issue Feb 28, 2024
Branch: refs/heads/main
Date: 2024-02-27T20:48:22-08:00
Author: Wesley Barroso Lopes (wesleybl) <[email protected]>
Commit: plone/plone.restapi@1771c9c

Use last version of Python 3.12 in tests (#1756)

python/cpython#113267 has been fixed.

Files changed:
A news/1740.internal
M .github/workflows/tests.yml
mister-roboto pushed a commit to plone/buildout.coredev that referenced this issue Feb 28, 2024
Branch: refs/heads/main
Date: 2024-02-27T20:48:22-08:00
Author: Wesley Barroso Lopes (wesleybl) <[email protected]>
Commit: plone/plone.restapi@1771c9c

Use last version of Python 3.12 in tests (#1756)

python/cpython#113267 has been fixed.

Files changed:
A news/1740.internal
M .github/workflows/tests.yml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.12 bugs and security fixes 3.13 bugs and security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

5 participants