Skip to content

Commit 21bc927

Browse files
committed
wip
1 parent 86b2632 commit 21bc927

File tree

3 files changed

+517
-0
lines changed

3 files changed

+517
-0
lines changed

tools/flakeguard/runner/example_test_package/example_tests_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,107 @@ func TestTimeout(t *testing.T) {
212212
time.Sleep(time.Until(deadline))
213213
t.Logf("This test should have timed out")
214214
}
215+
216+
// 1) No subtests at all
217+
func TestParentNoSubtests(t *testing.T) {
218+
t.Parallel()
219+
220+
t.Log("No subtests, just a single test that passes.")
221+
// (Optional) you could also do t.Fail() or t.Fatal() to produce a fail
222+
}
223+
224+
// 2) All subtests pass, no parent fail
225+
func TestParentAllPassSubtests(t *testing.T) {
226+
t.Parallel()
227+
t.Log("Parent does not fail, subtests all pass")
228+
229+
t.Run("SubtestA", func(t *testing.T) {
230+
t.Parallel()
231+
t.Log("passes")
232+
})
233+
t.Run("SubtestB", func(t *testing.T) {
234+
t.Parallel()
235+
t.Log("passes")
236+
})
237+
}
238+
239+
// 3) All subtests fail, no parent fail
240+
func TestParentAllFailSubtests(t *testing.T) {
241+
t.Parallel()
242+
t.Log("Parent does not fail, subtests all fail => typically the parent is marked fail by Go")
243+
244+
t.Run("FailA", func(t *testing.T) {
245+
t.Parallel()
246+
t.Fatal("This subtest always fails")
247+
})
248+
t.Run("FailB", func(t *testing.T) {
249+
t.Parallel()
250+
t.Fatal("This subtest always fails")
251+
})
252+
}
253+
254+
// 4) Some subtests pass, some fail, parent does NOT do its own fail
255+
func TestParentSomeFailSubtests(t *testing.T) {
256+
t.Parallel()
257+
t.Log("Parent does not fail, subtests partially pass/fail => parent is typically fail unless 'zeroOutParentFailsIfSubtestOnlyFails' modifies it")
258+
259+
t.Run("Pass", func(t *testing.T) {
260+
t.Parallel()
261+
t.Log("This subtest passes")
262+
})
263+
t.Run("Fail", func(t *testing.T) {
264+
t.Parallel()
265+
t.Fatal("This subtest fails")
266+
})
267+
}
268+
269+
// 5) Parent fails *after* subtests
270+
func TestParentOwnFailAfterSubtests(t *testing.T) {
271+
t.Parallel()
272+
t.Log("Parent fails after subtests pass => genuine parent-level failure")
273+
274+
t.Run("Pass1", func(t *testing.T) {
275+
t.Parallel()
276+
t.Log("This subtest always passes")
277+
})
278+
t.Run("Pass2", func(t *testing.T) {
279+
t.Parallel()
280+
t.Log("This subtest always passes")
281+
})
282+
283+
// Finally, parent fails
284+
t.Fatal("Parent test fails after subtests pass")
285+
}
286+
287+
// 6) Parent fails *before* subtests
288+
func TestParentOwnFailBeforeSubtests(t *testing.T) {
289+
t.Parallel()
290+
t.Log("Parent fails before subtests => subtests might not even run in real usage, or still get reported, depending on concurrency")
291+
292+
t.Fatal("Parent test fails immediately")
293+
294+
t.Run("WouldPassButNeverRuns", func(t *testing.T) {
295+
t.Parallel()
296+
t.Log("Normally passes, but might not even run now.")
297+
})
298+
}
299+
300+
// 7) Nested subtests: parent -> child -> grandchild
301+
func TestNestedSubtests(t *testing.T) {
302+
t.Parallel()
303+
t.Log("Deep nesting example")
304+
305+
t.Run("Level1", func(t *testing.T) {
306+
t.Parallel()
307+
308+
t.Run("Level2Pass", func(t *testing.T) {
309+
t.Parallel()
310+
t.Log("This sub-subtest passes")
311+
})
312+
313+
t.Run("Level2Fail", func(t *testing.T) {
314+
t.Parallel()
315+
t.Fatal("This sub-subtest fails")
316+
})
317+
})
318+
}

tools/flakeguard/runner/runner.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,10 @@ func (r *Runner) parseTestResults(filePaths []string) ([]reports.TestResult, err
565565
log.Warn().Str("parent test", parentTestKey).Msg("expected parent test not found")
566566
}
567567
}
568+
569+
// Zero out parent test's failures if they are purely caused by subtest fails
570+
zeroOutParentFailsIfSubtestOnlyFails(testDetails, testsWithSubTests)
571+
568572
for _, result := range testDetails {
569573
if result.Runs > expectedRuns { // Panics can introduce double-counting test failures, this is a correction for it
570574
if result.Panic {
@@ -595,6 +599,87 @@ func (r *Runner) parseTestResults(filePaths []string) ([]reports.TestResult, err
595599
return results, nil
596600
}
597601

602+
// zeroOutParentFailsIfSubtestOnlyFails scans through parent tests in `testDetails`.
603+
// If a parent test's failures only reference subtests (no genuine parent-level lines),
604+
// we zero out the parent's failures, so the parent is considered passed.
605+
func zeroOutParentFailsIfSubtestOnlyFails(
606+
testDetails map[string]*reports.TestResult,
607+
testsWithSubTests map[string][]string,
608+
) {
609+
for parentTestKey, subTests := range testsWithSubTests {
610+
parentResult, ok := testDetails[parentTestKey]
611+
if !ok {
612+
continue
613+
}
614+
// If the parent has zero failures, no need to adjust.
615+
if parentResult.Failures == 0 {
616+
continue
617+
}
618+
619+
parentHasOwnFailure := false
620+
621+
// For each run's fail lines
622+
for _, failLines := range parentResult.FailedOutputs {
623+
for _, line := range failLines {
624+
625+
// 1) If line doesn't even mention parent test name (e.g. "example_tests_test.go:315: This sub-subtest fails"),
626+
// skip it. It's presumably sub-subtest detail.
627+
if !strings.Contains(line, parentResult.TestName) {
628+
continue
629+
}
630+
631+
// 2) If it's the summary line, e.g. "--- FAIL: TestNestedSubtests (0.00s)",
632+
// skip it. It's not a genuine parent-level fail for your purposes.
633+
if strings.HasPrefix(line, "--- FAIL: "+parentResult.TestName+" (") {
634+
continue
635+
}
636+
637+
// 3) Check if it references "TestParent/SubtestName"
638+
// e.g. "TestNestedSubtests/Level1"
639+
isSubtestLine := false
640+
for _, subName := range subTests {
641+
expected := parentResult.TestName + "/" + subName
642+
if strings.Contains(line, expected) {
643+
isSubtestLine = true
644+
break
645+
}
646+
}
647+
648+
// If we *still* can’t identify it as purely subtest-based => it's a genuine parent-level fail.
649+
if !isSubtestLine {
650+
parentHasOwnFailure = true
651+
break
652+
}
653+
}
654+
if parentHasOwnFailure {
655+
break
656+
}
657+
}
658+
659+
// If the parent has no genuine failure lines, zero out its fail
660+
if !parentHasOwnFailure {
661+
parentResult.Runs -= parentResult.Failures
662+
parentResult.Failures = 0
663+
664+
// Adjust successes if needed
665+
if parentResult.Runs < parentResult.Successes {
666+
parentResult.Successes = parentResult.Runs
667+
}
668+
669+
// Recompute pass ratio
670+
if parentResult.Runs > 0 {
671+
parentResult.PassRatio = float64(parentResult.Successes) / float64(parentResult.Runs)
672+
} else {
673+
// If no runs remain, we consider it "passed"
674+
parentResult.PassRatio = 1
675+
}
676+
677+
// Clear the parent's failed outputs
678+
parentResult.FailedOutputs = make(map[string][]string)
679+
}
680+
}
681+
}
682+
598683
// attributePanicToTest properly attributes panics to the test that caused them.
599684
func attributePanicToTest(panicPackage string, panicEntries []entry) (test string, timeout bool, err error) {
600685
regexSanitizePanicPackage := filepath.Base(panicPackage)

0 commit comments

Comments
 (0)