-
Notifications
You must be signed in to change notification settings - Fork 12
Skip retries for pytest collection errors #492
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,19 +76,44 @@ func (r *RunResult) RecordTestResult(testCase plan.TestCase, status TestStatus) | |
| } | ||
| } | ||
|
|
||
| // RecordCollectionError records a test collection error (e.g. import failure). | ||
| // Collection errors are recorded as failed but excluded from retries. | ||
| func (r *RunResult) RecordCollectionError(testCase plan.TestCase) { | ||
| test := r.getTest(testCase) | ||
| test.Status = TestStatusFailed | ||
| test.CollectionError = true | ||
| test.ExecutionCount++ | ||
| if r.mutedTestLookup[mutedTestIdentifier(testCase)] { | ||
| test.Muted = true | ||
| } | ||
| } | ||
|
|
||
| // FailedTests returns a list of test cases that failed. | ||
| // Collection errors are excluded because retrying them is pointless | ||
| // (the underlying issue is a broken import or syntax error, not a flaky test). | ||
| func (r *RunResult) FailedTests() []plan.TestCase { | ||
| var failedTests []plan.TestCase | ||
|
|
||
| for _, test := range r.tests { | ||
| if test.Status == TestStatusFailed && !test.Muted { | ||
| if test.Status == TestStatusFailed && !test.Muted && !test.CollectionError { | ||
| failedTests = append(failedTests, test.TestCase) | ||
| } | ||
| } | ||
|
|
||
| return failedTests | ||
| } | ||
|
|
||
| // CollectionErrors returns test cases that failed due to collection errors (e.g. import failures). | ||
| func (r *RunResult) CollectionErrors() []plan.TestCase { | ||
| var errors []plan.TestCase | ||
| for _, test := range r.tests { | ||
| if test.CollectionError && !test.Muted { | ||
| errors = append(errors, test.TestCase) | ||
| } | ||
| } | ||
| return errors | ||
| } | ||
|
|
||
| func (r *RunResult) MutedTests() []TestResult { | ||
| var mutedTests []TestResult | ||
| for _, test := range r.tests { | ||
|
|
@@ -115,7 +140,7 @@ func (r *RunResult) FailedMutedTests() []plan.TestCase { | |
| var failedTests []plan.TestCase | ||
|
|
||
| for _, test := range r.tests { | ||
| if test.Status == TestStatusFailed && test.Muted { | ||
| if test.Status == TestStatusFailed && test.Muted && !test.CollectionError { | ||
| failedTests = append(failedTests, test.TestCase) | ||
| } | ||
| } | ||
|
|
@@ -132,6 +157,26 @@ func (r *RunResult) passedTestsCount() int { | |
| return count | ||
| } | ||
|
|
||
| func (r *RunResult) collectionErrorCount() int { | ||
| count := 0 | ||
| for _, test := range r.tests { | ||
| if test.CollectionError && !test.Muted { | ||
| count++ | ||
| } | ||
| } | ||
| return count | ||
| } | ||
|
|
||
| func (r *RunResult) mutedCollectionErrorCount() int { | ||
| count := 0 | ||
| for _, test := range r.tests { | ||
| if test.CollectionError && test.Muted { | ||
| count++ | ||
| } | ||
| } | ||
| return count | ||
| } | ||
|
|
||
| func (r *RunResult) skippedTestsCount() int { | ||
| count := 0 | ||
| for _, test := range r.tests { | ||
|
|
@@ -163,7 +208,7 @@ func (r *RunResult) OnlyMutedFailures() bool { | |
|
|
||
| // Status returns the overall status of the test run. | ||
| // If there is an error, it returns RunStatusError. | ||
| // If there are failed tests, it returns RunStatusFailed. | ||
| // If there are any non-muted failed tests (including collection errors), it returns RunStatusFailed. | ||
| // Otherwise, it returns RunStatusPassed. | ||
| func (r *RunResult) Status() RunStatus { | ||
| if r.error != nil { | ||
|
|
@@ -174,11 +219,11 @@ func (r *RunResult) Status() RunStatus { | |
| return RunStatusUnknown | ||
| } | ||
|
|
||
| if len(r.FailedTests()) > 0 { | ||
| if len(r.FailedTests()) > 0 || r.collectionErrorCount() > 0 { | ||
| return RunStatusFailed | ||
| } | ||
|
|
||
| if r.passedTestsCount()+r.skippedTestsCount()+len(r.FailedMutedTests()) == len(r.tests) { | ||
| if r.passedTestsCount()+r.skippedTestsCount()+len(r.FailedMutedTests())+r.mutedCollectionErrorCount() == len(r.tests) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's even possible to mute an entire set of tests so I don't think this makes sense, but I could be wrong. In other words, a collection error could mean one or more tests that may or may not be muted. I think it's far simpler to simply treat all collection failures as true failures and to exclude this mutedCollectionErrorCount from this completely.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah we can't mute at the test file level. And yeah we should treat collection failures as true failures and exclude the mutedCollectionErrorCount from this completely. |
||
| return RunStatusPassed | ||
| } | ||
|
|
||
|
|
@@ -241,8 +286,9 @@ type TestEngineTest struct { | |
| Name string | ||
| Scope string | ||
| Location string | ||
| FileName string `json:"file_name,omitempty"` | ||
| FileName string `json:"file_name,omitempty"` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 |
||
| Result TestStatus | ||
| Tags map[string]string `json:"tags,omitempty"` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 |
||
| } | ||
|
|
||
| func parseTestEngineTestResult(path string) ([]TestEngineTest, error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| [ | ||
| {"id": "a1be7e52-0dba-4018-83ce-a1598ca68807", "scope": "tests/failed_test.py", "name": "test_failed", "location": "tests/failed_test.py:0", "file_name": "tests/failed_test.py", "result": "failed", "failure_reason": "def test_failed():\n> assert 3 == 5\nE assert 3 == 5\n\ntests/failed_test.py:2: AssertionError"}, | ||
| {"id": "c0ffee00-dead-beef-cafe-123456789abc", "scope": "tests/broken_import.py", "name": "tests/broken_import.py", "location": "tests/broken_import.py", "file_name": "tests/broken_import.py", "result": "failed", "failure_reason": "ModuleNotFoundError: No module named 'nonexistent'", "tags": {"test.pytest_collection_error": "true"}} | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| [ | ||
| { | ||
| "id": "db2c4ffa-efee-4b6a-b293-a4c4168aa353", | ||
| "scope": "tests/test_sample.py", | ||
| "name": "test_happy", | ||
| "location": "tests/test_sample.py:0", | ||
| "file_name": "tests/test_sample.py", | ||
| "result": "passed" | ||
| }, | ||
| { | ||
| "id": "c0ffee00-dead-beef-cafe-123456789abc", | ||
| "scope": "tests/broken_import.py", | ||
| "name": "tests/broken_import.py", | ||
| "location": "tests/broken_import.py", | ||
| "file_name": "tests/broken_import.py", | ||
| "result": "failed", | ||
| "tags": {"test.pytest_collection_error": "true"} | ||
| } | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't know if this makes sense or not. It's likely it does given a collection error typically means one or more tests (even if muted) can't be run so it's probably a good idea to bail early and treat collection errors as things that can't be muted/ignored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think that's right