Skip to content

Commit ba16dd0

Browse files
committed
Refactor panic and race attribution functions in Flakeguard: update regex patterns for improved accuracy, change function names to exported versions, and enhance error handling for better clarity in failure cases.
1 parent 9b91339 commit ba16dd0

File tree

3 files changed

+281
-58
lines changed

3 files changed

+281
-58
lines changed

tools/flakeguard/runner/parser/attribution.go

Lines changed: 40 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,14 @@ import (
1111
)
1212

1313
var (
14-
// Regex to extract a valid test function name from a panic message.
15-
// This is the most common situation for test panics, e.g.
16-
// github.com/smartcontractkit/chainlink/deployment/keystone/changeset_test.TestDeployBalanceReader(0xc000583c00)
17-
nestedTestNameRe = regexp.MustCompile(`\.(Test[^\s(]+)`) // Simpler regex, matches TestName directly after a dot
18-
19-
// Regex to check if the panic is from a log after a goroutine, e.g.
20-
// panic: Log in goroutine after Test_workflowRegisteredHandler/skips_fetch_if_secrets_url_is_missing has completed: <Log line>
14+
// Use the more precise original regex to avoid capturing .func suffixes
15+
nestedTestNameRe = regexp.MustCompile(`\.(Test[^\s]+?)(?:\.[^(]+)?\s*\(`)
16+
// Other regexes remain the same
2117
testLogAfterTestRe = regexp.MustCompile(`^panic: Log in goroutine after (Test[^\s]+) has completed:`)
18+
didTestTimeoutRe = regexp.MustCompile(`^panic: test timed out after ([^\s]+)`)
19+
timedOutTestNameRe = regexp.MustCompile(`^\s*(Test[^\s]+)\s+\((.*)\)`)
2220

23-
// Check if the panic message indicates a timeout, e.g.
24-
// panic: test timed out after 10m0s
25-
didTestTimeoutRe = regexp.MustCompile(`^panic: test timed out after ([^\s]+)`)
26-
// Regex to extract a valid test function name from a panic message if the panic is a timeout, e.g.
27-
// TestTimedOut (10m0s)
28-
timedOutTestNameRe = regexp.MustCompile(`^\s*(Test[^\s]+)\s+\((.*)\)`) // Added optional leading space
29-
21+
// Exported Errors
3022
ErrFailedToAttributePanicToTest = errors.New("failed to attribute panic to test")
3123
ErrFailedToAttributeRaceToTest = errors.New("failed to attribute race to test")
3224
ErrFailedToParseTimeoutDuration = errors.New("failed to parse timeout duration")
@@ -36,13 +28,12 @@ var (
3628
ErrDetectedTimeoutFailedAttribution = errors.New("detected test timeout, but failed to attribute the timeout to a specific test")
3729
)
3830

39-
// attributePanicToTest properly attributes panics to the test that caused them.
40-
// There are a lot of edge cases and strange behavior in Go test output when it comes to panics.
41-
func attributePanicToTest(outputs []string) (test string, timeout bool, err error) {
31+
// AttributePanicToTest properly attributes panics to the test that caused them.
32+
func AttributePanicToTest(outputs []string) (test string, timeout bool, err error) {
4233
var (
4334
timeoutDurationStr string
4435
timeoutDuration time.Duration
45-
foundTestName string // Store first plausible test name found
36+
foundTestName string // Store first plausible test name found as fallback
4637
)
4738

4839
for _, output := range outputs {
@@ -64,12 +55,10 @@ func attributePanicToTest(outputs []string) (test string, timeout bool, err erro
6455
var parseErr error
6556
timeoutDuration, parseErr = time.ParseDuration(timeoutDurationStr)
6657
if parseErr != nil {
67-
// Log error but continue searching, maybe timeout reported differently later
6858
log.Warn().Str("duration_str", timeoutDurationStr).Err(parseErr).Msg("Failed to parse timeout duration from initial panic line")
69-
// Return error immediately? Or hope timedOutTestNameRe finds it? Let's return error.
59+
// Use Errorf here to wrap the error
7060
return "", true, fmt.Errorf("%w: %w using output line: %s", ErrFailedToParseTimeoutDuration, parseErr, output)
7161
}
72-
// Don't return yet, need to find the specific timed-out test name below
7362
log.Debug().Dur("duration", timeoutDuration).Msg("Detected timeout panic")
7463
continue // Continue scanning for the test name line
7564
}
@@ -82,78 +71,75 @@ func attributePanicToTest(outputs []string) (test string, timeout bool, err erro
8271
testDuration, parseErr := time.ParseDuration(testDurationStr)
8372
if parseErr != nil {
8473
log.Warn().Str("test", testName).Str("duration_str", testDurationStr).Err(parseErr).Msg("Failed to parse duration from timed-out test line")
85-
// If we already have a timeoutDuration, maybe use this test name anyway?
86-
// Let's continue searching for a perfect match first. Store this as potential.
87-
if foundTestName == "" {
88-
foundTestName = testName
89-
}
74+
// If duration parsing fails for a candidate test, immediately return a specific error
75+
return "", true, fmt.Errorf("%w: test '%s' listed with unparseable duration '%s': %w", ErrDetectedTimeoutFailedParse, testName, testDurationStr, parseErr)
9076
} else if testDuration >= timeoutDuration {
91-
// Found the test that likely caused the timeout
9277
log.Debug().Str("test", testName).Dur("test_duration", testDuration).Dur("timeout_duration", timeoutDuration).Msg("Attributed timeout panic via duration match")
93-
return testName, true, nil // Found definitive match
78+
return testName, true, nil // Found a valid match!
9479
} else {
9580
log.Debug().Str("test", testName).Dur("test_duration", testDuration).Dur("timeout_duration", timeoutDuration).Msg("Ignoring test line, duration too short for timeout")
9681
}
9782
}
9883
}
9984

100-
// General check for test names within stack trace lines (less reliable but a fallback)
101-
if match := nestedTestNameRe.FindStringSubmatch(output); len(match) > 1 {
102-
testName := strings.TrimSpace(match[1])
103-
// Avoid standard library test runners or internal functions
85+
// General check for test names using the more precise regex
86+
matchNestedTestName := nestedTestNameRe.FindStringSubmatch(output)
87+
if len(matchNestedTestName) > 1 {
88+
testName := strings.TrimSpace(matchNestedTestName[1]) // Group 1 captures the core test name
10489
if !strings.HasPrefix(testName, "Test") {
105-
continue
90+
continue // Should not happen with this regex, but safety check
10691
}
107-
// Prioritize longer, more specific names if multiple matches found?
108-
// For now, store the first plausible one found if we haven't found one yet.
92+
// Store the first plausible name found as a fallback
10993
if foundTestName == "" {
11094
log.Debug().Str("test", testName).Str("line", output).Msg("Found potential test name in panic output")
11195
foundTestName = testName
112-
// Don't return yet, keep searching for more specific patterns (like timeout or log after test)
11396
}
11497
}
11598
} // End loop over outputs
11699

117100
// Post-loop evaluation
118101
if timeout {
102+
// If we reach here, timeout was detected, but NO line matched BOTH name and duration threshold.
103+
// Return the generic attribution failure error.
104+
var errMsg string
119105
if foundTestName != "" {
120-
// If timeout was detected, and we found a potential test name (maybe without duration match), use it.
121-
log.Warn().Str("test", foundTestName).Msg("Attributing timeout to test name found, but duration match was inconclusive or missing.")
122-
return foundTestName, true, nil
106+
// Include the fallback name if found, even though its duration didn't match/parse.
107+
errMsg = fmt.Sprintf("timeout duration %s detected, found candidate test '%s' but duration did not meet threshold or failed parsing earlier", timeoutDurationStr, foundTestName)
108+
} else {
109+
errMsg = fmt.Sprintf("timeout duration %s detected, but no matching test found in output", timeoutDurationStr)
123110
}
124-
// If timeout detected but no test name found anywhere
125-
return "", true, fmt.Errorf("%w in package context using output:\n%s", ErrDetectedTimeoutFailedAttribution, strings.Join(outputs, "\n"))
111+
return "", true, fmt.Errorf("%w: %s: %w", ErrDetectedTimeoutFailedAttribution, errMsg, errors.New(strings.Join(outputs, "\n")))
126112
}
127113

128114
if foundTestName != "" {
129-
// If not a timeout, but we found a test name in the stack trace
115+
// If not a timeout, but we found a test name via the general regex
130116
log.Debug().Str("test", foundTestName).Msg("Attributed non-timeout panic via test name found in stack")
131117
return foundTestName, false, nil
132118
}
133119

134-
// If we reach here, no pattern matched successfully
135-
return "", false, fmt.Errorf("%w using output:\n%s", ErrFailedToAttributePanicToTest, strings.Join(outputs, "\n"))
120+
// If we reach here, no pattern matched successfully for non-timeout panic
121+
// Use Errorf for the final error wrapping
122+
return "", false, fmt.Errorf("%w: using output: %w", ErrFailedToAttributePanicToTest, errors.New(strings.Join(outputs, "\n")))
136123
}
137124

138-
// attributeRaceToTest properly attributes races to the test that caused them.
139-
// Race output often includes stack traces mentioning the test function.
140-
func attributeRaceToTest(outputs []string) (string, error) {
125+
// AttributeRaceToTest properly attributes races to the test that caused them.
126+
func AttributeRaceToTest(outputs []string) (string, error) {
141127
for _, output := range outputs {
142128
output = strings.TrimSpace(output)
143129
if output == "" {
144130
continue
145131
}
146-
// Use the same regex as panic attribution fallback
147-
if match := nestedTestNameRe.FindStringSubmatch(output); len(match) > 1 {
148-
testName := strings.TrimSpace(match[1])
132+
// Use the precise regex here too
133+
match := nestedTestNameRe.FindStringSubmatch(output)
134+
if len(match) > 1 {
135+
testName := strings.TrimSpace(match[1]) // Group 1 captures the core test name
149136
if strings.HasPrefix(testName, "Test") {
150137
log.Debug().Str("test", testName).Str("line", output).Msg("Attributed race via test name match")
151138
return testName, nil
152139
}
153140
}
154141
}
155-
// If no match found in any line
156-
return "", fmt.Errorf("%w using output:\n%s",
157-
ErrFailedToAttributeRaceToTest, strings.Join(outputs, "\n"),
158-
)
142+
// Use Errorf for the final error wrapping if loop completes without match
143+
return "", fmt.Errorf("%w: using output: %w",
144+
ErrFailedToAttributeRaceToTest, errors.New(strings.Join(outputs, "\n")))
159145
}

0 commit comments

Comments
 (0)