Description
When using pytest with pytest-subtests, bktec's muted test exit code suppression (OnlyMutedFailures()) does not work. The build exits with code 1 even when the only test failure is from a muted test.
Environment
- bktec version: v2.3.0
- Test runner:
pytest with pytest-subtests and pytest-xdist
- buildkite-test-collector for pytest
Root Cause Analysis
I traced through the bktec source code and identified the issue:
1. RunResult.Status() returns RunStatusUnknown
In internal/runner/run_result.go:168-186, Status() computes the run status by counting tests in known status buckets:
func (r *RunResult) Status() RunStatus {
if r.error != nil { return RunStatusError }
if len(r.tests) == 0 { return RunStatusUnknown }
if len(r.FailedTests()) > 0 { return RunStatusFailed }
if r.passedTestsCount()+r.skippedTestsCount()+len(r.FailedMutedTests()) == len(r.tests) {
return RunStatusPassed
}
return RunStatusUnknown // ← falls through here
}
The only recognized TestStatus values are "passed", "failed", and "skipped" (internal/runner/test_result.go:7-11).
When pytest-subtests is used, the buildkite-test-collector pytest plugin writes subtest results to the --json output file. These subtest results may have statuses that don't match the three recognized values. Since these tests are recorded in r.tests but don't count toward passedTestsCount, skippedTestsCount, or FailedMutedTests, the sum check fails and Status() falls through to RunStatusUnknown.
2. OnlyMutedFailures() bails out on RunStatusUnknown
In internal/runner/run_result.go:145-148:
func (r *RunResult) OnlyMutedFailures() bool {
if r.Status() == RunStatusError || r.Status() == RunStatusUnknown {
return false // ← always false when Status() is Unknown
}
// ...
}
3. Exit code suppression doesn't fire
In internal/command/run.go:102-110:
if exitError.ExitCode() == 1 && runResult.OnlyMutedFailures() {
return nil // ← never reached because OnlyMutedFailures() returned false
}
return fmt.Errorf("%s exited with error: %w", testRunner.Name(), runErr) // ← exit code 1 propagated
Observed Behavior
In a real CI build:
- pytest reported:
2 failed, 307 passed, 48 skipped, 331 warnings, 735 subtests passed
- The "2 failed" are both from the same muted test (1 SUBFAILED subtest + 1 FAILED parent from
pytest-subtests)
- No bktec report was printed (confirming
Status() returned RunStatusUnknown and printReport returned early)
- bktec output:
pytest exited with error: exit status 1
- Build failed despite the only failure being a muted test
Expected Behavior
When the only test failures are from muted tests, bktec should exit with code 0 regardless of whether pytest-subtests is used.
Suggested Fix
The Status() function could handle unrecognized test statuses more gracefully. Options:
- Ignore unrecognized statuses in the sum check — only count tests with recognized statuses toward the total
- Treat unrecognized statuses as a fourth bucket — add them to the sum so the accounting still works
- Map subtest statuses to parent statuses — normalize any unrecognized status to the closest standard one
Option 1 seems simplest and least risky — the sum check at the end of Status() would exclude tests with unrecognized statuses from both sides of the equation.
Reproduction
- Create a pytest test suite using
pytest-subtests
- Mute a test that has subtests in Buildkite Test Engine
- Run with bktec v2.3.0 and
--json={{resultPath}}
- Observe that when the muted test fails, bktec exits with code 1 instead of 0
Description
When using
pytestwithpytest-subtests, bktec's muted test exit code suppression (OnlyMutedFailures()) does not work. The build exits with code 1 even when the only test failure is from a muted test.Environment
pytestwithpytest-subtestsandpytest-xdistRoot Cause Analysis
I traced through the bktec source code and identified the issue:
1.
RunResult.Status()returnsRunStatusUnknownIn
internal/runner/run_result.go:168-186,Status()computes the run status by counting tests in known status buckets:The only recognized
TestStatusvalues are"passed","failed", and"skipped"(internal/runner/test_result.go:7-11).When
pytest-subtestsis used, thebuildkite-test-collectorpytest plugin writes subtest results to the--jsonoutput file. These subtest results may have statuses that don't match the three recognized values. Since these tests are recorded inr.testsbut don't count towardpassedTestsCount,skippedTestsCount, orFailedMutedTests, the sum check fails andStatus()falls through toRunStatusUnknown.2.
OnlyMutedFailures()bails out onRunStatusUnknownIn
internal/runner/run_result.go:145-148:3. Exit code suppression doesn't fire
In
internal/command/run.go:102-110:Observed Behavior
In a real CI build:
2 failed, 307 passed, 48 skipped, 331 warnings, 735 subtests passedpytest-subtests)Status()returnedRunStatusUnknownandprintReportreturned early)pytest exited with error: exit status 1Expected Behavior
When the only test failures are from muted tests, bktec should exit with code 0 regardless of whether
pytest-subtestsis used.Suggested Fix
The
Status()function could handle unrecognized test statuses more gracefully. Options:Option 1 seems simplest and least risky — the sum check at the end of
Status()would exclude tests with unrecognized statuses from both sides of the equation.Reproduction
pytest-subtests--json={{resultPath}}