Skip to content

Commit 6af8024

Browse files
committed
Never fail parent test if any subtest failed
1 parent eaef14f commit 6af8024

File tree

2 files changed

+95
-176
lines changed

2 files changed

+95
-176
lines changed

tools/flakeguard/go-test-transform/pkg/transformer/transformer.go

Lines changed: 43 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ type TestNode struct {
3030
Children map[string]*TestNode // Child tests
3131
Parent *TestNode // Parent test
3232
IsSubtest bool // Is this a subtest
33-
// Track whether this node is directly matched by patterns
34-
DirectlyIgnored bool
3533
// Store output messages for direct failure detection
3634
OutputMessages []string
35+
// Flag to track if this node has any failing subtests
36+
HasFailingSubtests bool
3737
}
3838

3939
// TransformJSON transforms go test -json output according to the options
@@ -62,11 +62,11 @@ func TransformJSON(input io.Reader, output io.Writer, opts *Options) (int, error
6262
// Track output messages for direct failure detection
6363
captureOutputMessages(events, testTree)
6464

65-
// Apply ignore rules
66-
applyIgnoreRules(testTree, opts)
65+
// Mark parents with failing subtests
66+
markParentsWithFailingSubtests(testTree)
6767

68-
// Propagate ignore status - but only in specific ways
69-
propagateIgnoreStatus(testTree, opts)
68+
// Identify which tests should be passed
69+
identifyTestsToIgnore(testTree, opts)
7070

7171
// Transform events
7272
transformedEvents, anyRemainingFailures := transformEvents(events, testTree)
@@ -228,121 +228,58 @@ func captureOutputMessages(events []TestEvent, tree map[string]*TestNode) {
228228
}
229229
}
230230

231-
// applyIgnoreRules applies ignore rules to the test tree
232-
func applyIgnoreRules(tree map[string]*TestNode, opts *Options) {
231+
// markParentsWithFailingSubtests marks parent nodes that have any failing subtests
232+
func markParentsWithFailingSubtests(tree map[string]*TestNode) {
233233
// Process each package
234234
for _, pkgNode := range tree {
235-
// Process all tests in this package recursively
236-
var processNode func(*TestNode)
237-
processNode = func(node *TestNode) {
238-
// Only need to check if it's a failing test
239-
if node.Failed {
240-
// Apply ignore rules
241-
if node.IsSubtest && opts.IgnoreAllSubtestFailures {
242-
node.Ignored = true
243-
node.DirectlyIgnored = true
244-
}
245-
}
235+
// Bottom-up traversal to mark parents with failing subtests
236+
var markParents func(*TestNode) bool
237+
markParents = func(node *TestNode) bool {
238+
// Check if any child is failing or has failing subtests
239+
hasFailingSubtests := false
246240

247-
// Recursively process children
248241
for _, child := range node.Children {
249-
processNode(child)
242+
// Process child first (depth-first)
243+
childHasFailingSubtests := markParents(child)
244+
245+
// Check if child is failing or has failing subtests
246+
if child.Failed || childHasFailingSubtests {
247+
hasFailingSubtests = true
248+
}
250249
}
250+
251+
// Update the node's status
252+
node.HasFailingSubtests = hasFailingSubtests
253+
254+
// Return whether this node has failing subtests
255+
return hasFailingSubtests
251256
}
252257

253-
processNode(pkgNode)
258+
markParents(pkgNode)
254259
}
255260
}
256261

257-
// propagateIgnoreStatus propagates ignore status up the tree
258-
func propagateIgnoreStatus(tree map[string]*TestNode, opts *Options) {
262+
// identifyTestsToIgnore identifies which tests should be converted to pass
263+
func identifyTestsToIgnore(tree map[string]*TestNode, opts *Options) {
259264
// Process each package
260265
for _, pkgNode := range tree {
261-
// We need to be careful how we propagate status
262-
// Only propagate from children to parent when ALL failing children are ignored
263-
264-
// Bottom-up traversal
265-
var markIgnored func(*TestNode) bool
266-
markIgnored = func(node *TestNode) bool {
267-
// First process all children
268-
allFailingChildrenIgnored := true
269-
anyFailingChildren := false
270-
266+
// Process all nodes in the tree and set ignore status
267+
var processNode func(*TestNode)
268+
processNode = func(node *TestNode) {
269+
// Process children first
271270
for _, child := range node.Children {
272-
if child.Failed {
273-
anyFailingChildren = true
274-
if !markIgnored(child) {
275-
allFailingChildrenIgnored = false
276-
}
277-
} else {
278-
// Make sure to process non-failing children too
279-
markIgnored(child)
280-
}
271+
processNode(child)
281272
}
282273

283-
// Now decide for this node
284-
if node.Failed {
285-
// If explicitly ignored, return that status
286-
if node.DirectlyIgnored || node.Ignored {
287-
return true
288-
}
289-
290-
// If this is a parent and ALL failing children are ignored,
291-
// Decide if we should ignore the parent
292-
if anyFailingChildren && allFailingChildrenIgnored {
293-
// For TestNestedSubtests, we should always propagate
294-
if containsNestedFail(node) || containsParallel(node) {
295-
node.Ignored = true
296-
return true
297-
}
298-
299-
// For IgnoreAllSubtestFailures, we should always propagate
300-
if opts.IgnoreAllSubtestFailures {
301-
node.Ignored = true
302-
return true
303-
}
304-
}
305-
306-
// Default case: not ignored
307-
return false
274+
// If this node has failing subtests and it's failing itself,
275+
// mark it as ignored (so it will be converted to PASS)
276+
if node.HasFailingSubtests && node.Failed {
277+
node.Ignored = true
308278
}
309-
310-
// Node isn't failed, so it's "ignored" for propagation purposes
311-
return true
312-
}
313-
314-
markIgnored(pkgNode)
315-
}
316-
}
317-
318-
// containsNestedFail checks if this node or any of its children contain NestedFail
319-
func containsNestedFail(node *TestNode) bool {
320-
if strings.Contains(node.Name, "NestedFail") {
321-
return true
322-
}
323-
324-
for _, child := range node.Children {
325-
if containsNestedFail(child) {
326-
return true
327279
}
328-
}
329-
330-
return false
331-
}
332280

333-
// containsParallel checks if this node or any of its children contain Parallel
334-
func containsParallel(node *TestNode) bool {
335-
if strings.Contains(node.Name, "Parallel") {
336-
return true
337-
}
338-
339-
for _, child := range node.Children {
340-
if containsParallel(child) {
341-
return true
342-
}
281+
processNode(pkgNode)
343282
}
344-
345-
return false
346283
}
347284

348285
// transformEvents applies transformations to events based on the test tree
@@ -386,18 +323,6 @@ func transformEvents(events []TestEvent, tree map[string]*TestNode) ([]TestEvent
386323
return nil
387324
}
388325

389-
// Helper function to check if a node has direct failure messages
390-
hasDirectFailure := func(node *TestNode) bool {
391-
for _, msg := range node.OutputMessages {
392-
// Check if the message indicates a direct failure (not just reporting a child failure)
393-
// This is a simple heuristic - direct failures usually don't mention child tests
394-
if !strings.Contains(msg, "===") && !strings.Contains(msg, "---") {
395-
return true
396-
}
397-
}
398-
return false
399-
}
400-
401326
for i, event := range events {
402327
// Make a copy of the event
403328
transformedEvents[i] = event
@@ -406,16 +331,8 @@ func transformEvents(events []TestEvent, tree map[string]*TestNode) ([]TestEvent
406331
if event.Action == "fail" {
407332
node := findNode(event.Package, event.Test)
408333
if node != nil && node.Ignored {
409-
// For leaf subtests, keep them as failed
410-
// For parent tests with direct failures, keep them as failed
411-
// For parent tests that only fail because of child failures, change to pass
412-
if (node.IsSubtest && len(node.Children) == 0) || hasDirectFailure(node) {
413-
// This is a leaf subtest or has a direct failure - keep it as a failure
414-
anyRemainingFailures = true
415-
} else {
416-
// This is a parent test without direct failures - change to pass
417-
transformedEvents[i].Action = "pass"
418-
}
334+
// Convert this fail to a pass
335+
transformedEvents[i].Action = "pass"
419336
} else {
420337
// We're keeping this as a failure
421338
anyRemainingFailures = true
@@ -426,10 +343,8 @@ func transformEvents(events []TestEvent, tree map[string]*TestNode) ([]TestEvent
426343
} else if event.Action == "output" {
427344
node := findNode(event.Package, event.Test)
428345
if node != nil && node.Failed && node.Ignored {
429-
// Only transform output text for non-subtests or parent tests without direct failures
430-
if !node.IsSubtest && !hasDirectFailure(node) {
431-
transformedEvents[i].Output = transformOutputText(event.Output)
432-
}
346+
// Transform output text for passed tests
347+
transformedEvents[i].Output = transformOutputText(event.Output)
433348
}
434349
}
435350
}

0 commit comments

Comments
 (0)