#35849 closed Cleanup/optimization (fixed)
ParallelTestSuite reports error occurred in arbitrary test instead of setUpClass
Reported by: | David Winiecki | Owned by: | David Winiecki |
---|---|---|---|
Component: | Testing framework | Version: | dev |
Severity: | Normal | Keywords: | |
Cc: | David Winiecki, Adam Johnson, Jacob Walls | Triage Stage: | Accepted |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
When running tests in parallel, if an error occurs in a test suite (TestCase
) before the first test method runs, for example in setUpClass
, then the shell output incorrectly states that the error occurred in an arbitrary test method, rather than, for example, setUpClass
.
This is a problem because a user reading the test output may not notice the real error location (for example, setUpClass
) in the traceback and may not run the other tests in the test suite.
This is especially problematic if automation parses the error location to automatically re-run tests (to address flaky, intermittently failing tests), because the other tests in the suite may never be run.
Repro:
- Create a Django project for testing:
Instructions to create the project yourself are below. Or you can use this existing project in the bugdemo/
directory at https://github.com/dcki/django/blob/demo_report_test_error_bug/bugdemo/README.md
To create the project yourself:
- Install Python.
- Create a virtualenv and activate it:
mkdir my_project && cd my_project && python3 -m venv .venv && . .venv/bin/activate
- Install Django and create a Django project:
pip install django && django-admin startproject mysite .
- Create a tests directory and an
__init__.py
file inside:mkdir mysite/tests && touch mysite/tests/__init__.py
- Install tblib:
pip install tblib
- Add these tests to the project:
mysite/tests/test_things.py
from django.test import SimpleTestCase class AlwaysFailTest(SimpleTestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() try: raise Exception('Intentional error') except: super().tearDownClass() raise def test_should_pass_a(self) -> None: pass def test_should_pass_b(self) -> None: pass # Exists so that, when an attempt is made to run tests in parallel, and at # least two TestCases are specified, then tests actually run in parallel. # (As of this writing, Django runs tests serially if there is only one test # suite to run, even if `--parallel=2` is specified.) class AlwaysPassTest(SimpleTestCase): def test_should_pass_a(self) -> None: pass def test_should_pass_b(self) -> None: pass
- Run the tests:
python3 manage.py test --parallel=2
Expected output:
Found 4 test(s). System check identified no issues (0 silenced). E.. ====================================================================== ERROR: setUpClass (mysite.tests.test_things.AlwaysFailTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/dwiniecki/.pyenv/versions/3.11.9/lib/python3.11/unittest/suite.py", line 166, in _handleClassSetUp setUpClass() File "/Users/dwiniecki/src/other/dcki-django/bugdemo2/mysite/tests/test_things.py", line 9, in setUpClass raise Exception('Intentional error') ^^^^^^^^^^^^^^^^^ Exception: Intentional error ---------------------------------------------------------------------- Ran 2 tests in 0.214s FAILED (errors=1)
Actual output:
Found 4 test(s). System check identified no issues (0 silenced). E.. ====================================================================== ERROR: test_should_pass_b (mysite.tests.test_things.AlwaysFailTest.test_should_pass_b) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/dwiniecki/.pyenv/versions/3.11.9/lib/python3.11/unittest/suite.py", line 166, in _handleClassSetUp setUpClass() File "/Users/dwiniecki/src/other/dcki-django/bugdemo/mysite/tests/test_things.py", line 9, in setUpClass raise Exception('Intentional error') ^^^^^^^^^^^^^^^^^ Exception: Intentional error ---------------------------------------------------------------------- Ran 2 tests in 0.221s FAILED (errors=1)
Change History (28)
comment:2 by , 2 months ago
Some of this work was done personally and some of it was done for my employer https://www.techsmart.codes/
We don't believe we've submitted a "Corporate Contributor License Agreement" and I haven't submitted an "Individual Contributor License Agreement". Another team member at TechSmart intends to submit a CCLA in a few days or so.
comment:3 by , 2 months ago
Cc: | added |
---|
comment:4 by , 2 months ago
I think this bug has existed since ParallelTestSuite was first written. I've confirmed that it exists at least as far back as 4.2. (But personally I don't feel like it's important enough to backport to earlier versions of Django. EDIT: Earlier than dev or 5.1 for example, that is.)
As far as I've seen there is no related documentation to update.
comment:5 by , 2 months ago
I'm tempted to check the "has patch" box but I'm not sure if it's too early to do that. :)
comment:6 by , 2 months ago
Cc: | added |
---|---|
Triage Stage: | Unreviewed → Accepted |
Type: | Bug → Cleanup/optimization |
Version: | 5.0 → dev |
Hello David, thank you for your detailed report. I have reproduced the issue as you indicated.
I find the distinction between this being an issue or not somewhat ambiguous. In my view, the first test run "is" the one that fails... but what leads me to accept this ticket is the fact that the error reporting differs depending on whether parallel=1
is set or not.
I will include a few folks in the ticket to see what they recommend, I took an initial look at your PR but I think we need more expert eyes on it.
comment:8 by , 2 months ago
I created a pull request **from the fork to itself** (not to upstream), just to have a place for discussion in the context of code. If this is not useful then I'll close the pull request. I may delete this fork later so if having historical context is important, then maybe using this pull request is not a good idea.
My impression is that I shouldn't create a PR to upstream until the proposed git branch is almost ready to merge, but maybe that's not true.
comment:9 by , 2 months ago
It makes sense to me to clean up the discrepancy between parallel=1
and parallel=n
, if we can do it easily enough. The single process runner uses TextTestRunner
, but the parallel one does not, so it makes sense that it's "on us" to mimic what TextTestRunner
does to get consistent test labels in this edge case. We might want to ensure unexpected success is covered just like test errors.
My impression is that I shouldn't create a PR to upstream until the proposed git branch is almost ready to merge, but maybe that's not true.
Appreciate you being careful, but I think it's totally appropriate to open a PR against upstream once the ticket's been accepted.
comment:10 by , 2 months ago
Has patch: | set |
---|
comment:11 by , 2 months ago
Okay awesome! I opened a real PR to upstream: https://github.com/django/django/pull/18695
We might want to ensure unexpected success is covered just like test errors.
I think I have an idea what you mean. If you have a chance to elaborate please do. I'll try to do more here soon.
Thank you all!
comment:12 by , 2 months ago
Sure, I just was musing that since unexpected successes are a flavor of error, we will want to be sure that their reporting doesn't change--since we still want failures to be labeled with the test name, not anything else--but that seems unlikely to have changed by your patch, so no worries.
comment:13 by , 2 months ago
I filled in the "branch description" section in the PR description.
but that seems unlikely to have changed by your patch
I think I understand. I'll try to learn more soon about unexpected successes and how they might apply to this.
comment:14 by , 2 months ago
Owner: | set to |
---|---|
Status: | new → assigned |
comment:15 by , 2 months ago
Regarding unexpected successes: It looks to me like unittest.TestCase.run()
is the only thing that calls result.addUnexpectedSuccess()
, and when I read the definition of run()
, it looks to me like it should not interact with the change proposed in the PR.
comment:16 by , 2 months ago
I'm not aware of any open questions currently, so I think it's ready for review when someone has time.
comment:17 by , 2 months ago
Thank you David, a reviewer will review it soon, but please note that Django moves at a slow pace, so it may be weeks.
comment:19 by , 2 months ago
Patch needs improvement: | set |
---|
comment:20 by , 2 months ago
Patch needs improvement: | unset |
---|
I think I responded to all current comments in the pull request and applied changes where decisions were made, so I don't have other changes pending currently. I can make more changes when we make more decisions.
I'm unchecking "patch needs improvement" for now. Please let me know if I shouldn't do that.
Thank you!
comment:21 by , 8 weeks ago
Patch needs improvement: | set |
---|
comment:22 by , 7 weeks ago
Patch needs improvement: | unset |
---|
comment:23 by , 7 weeks ago
Triage Stage: | Accepted → Ready for checkin |
---|
comment:27 by , 7 weeks ago
Triage Stage: | Ready for checkin → Accepted |
---|
My manager told me that he submitted a "Corporate Contributor License Agreement" Monday November 4th.
I wrote a proposed fix and regression test for this bug in a fork of the django GitHub project:
https://github.com/dcki/django/tree/ticket_35849
I want to help fix this but I'm not certain what the next step is. I read all the contributing documentation I could find.
tox
passes. (Excepttox -e docs
due to a problem withpyenchant
on my specific computer, butpip install pyenchant==3.3.0rc1 && cd docs && make spelling
passes.)