Skip to content

Commit 998add0

Browse files
authored
(feat) internal/civisibility: add Known Tests feature and refactor EFD logic (#3139)
1 parent dbfb8f2 commit 998add0

14 files changed

+367
-263
lines changed

internal/civisibility/constants/test_tags.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ const (
7878
// This constant is used to tag test events that are part of a retry execution
7979
TestIsRetry = "test.is_retry"
8080

81+
// TestRetryReason indicates the reason for retrying the test
82+
TestRetryReason = "test.retry_reason"
83+
8184
// TestEarlyFlakeDetectionRetryAborted indicates a retry abort reason by the early flake detection feature
8285
TestEarlyFlakeDetectionRetryAborted = "test.early_flake.abort_reason"
8386

internal/civisibility/integrations/civisibility_features.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ var (
5151
// ciVisibilitySettings contains the CI Visibility settings for this session
5252
ciVisibilitySettings net.SettingsResponseData
5353

54-
// ciVisibilityEarlyFlakyDetectionSettings contains the CI Visibility Early Flake Detection data for this session
55-
ciVisibilityEarlyFlakyDetectionSettings net.EfdResponseData
54+
// ciVisibilityKnownTests contains the CI Visibility Known Tests data for this session
55+
ciVisibilityKnownTests net.KnownTestsResponseData
5656

5757
// ciVisibilityFlakyRetriesSettings contains the CI Visibility Flaky Retries settings for this session
5858
ciVisibilityFlakyRetriesSettings FlakyRetriesSetting
@@ -121,15 +121,20 @@ func ensureAdditionalFeaturesInitialization(serviceName string) {
121121
return
122122
}
123123

124-
// if early flake detection is enabled then we run the early flake detection request
125-
if ciVisibilitySettings.EarlyFlakeDetection.Enabled {
126-
ciEfdData, err := ciVisibilityClient.GetEarlyFlakeDetectionData()
124+
// if early flake detection is enabled then we run the known tests request
125+
if ciVisibilitySettings.KnownTestsEnabled {
126+
ciEfdData, err := ciVisibilityClient.GetKnownTests()
127127
if err != nil {
128-
log.Error("civisibility: error getting CI visibility early flake detection data: %v", err)
128+
log.Error("civisibility: error getting CI visibility known tests data: %v", err)
129129
} else if ciEfdData != nil {
130-
ciVisibilityEarlyFlakyDetectionSettings = *ciEfdData
131-
log.Debug("civisibility: early flake detection data loaded.")
130+
ciVisibilityKnownTests = *ciEfdData
131+
log.Debug("civisibility: known tests data loaded.")
132132
}
133+
} else {
134+
// "known_tests_enabled" parameter works as a kill-switch for EFD, so if “known_tests_enabled” is false it
135+
// will disable EFD even if “early_flake_detection.enabled” is set to true (which should not happen normally,
136+
// the backend should disable both of them in that case)
137+
ciVisibilitySettings.EarlyFlakeDetection.Enabled = false
133138
}
134139

135140
// if flaky test retries is enabled then let's load the flaky retries settings
@@ -172,11 +177,11 @@ func GetSettings() *net.SettingsResponseData {
172177
return &ciVisibilitySettings
173178
}
174179

175-
// GetEarlyFlakeDetectionSettings gets the early flake detection known tests data
176-
func GetEarlyFlakeDetectionSettings() *net.EfdResponseData {
180+
// GetKnownTests gets the known tests data
181+
func GetKnownTests() *net.KnownTestsResponseData {
177182
// call to ensure the additional features initialization is completed (service name can be null here)
178183
ensureAdditionalFeaturesInitialization("")
179-
return &ciVisibilityEarlyFlakyDetectionSettings
184+
return &ciVisibilityKnownTests
180185
}
181186

182187
// GetFlakyRetriesSettings gets the flaky retries settings

internal/civisibility/integrations/gotesting/coverage/coverage_writer_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ type MockClient struct {
7373
SendCoveragePayloadFunc func(ciTestCovPayload io.Reader) error
7474
SendCoveragePayloadWithFormatFunc func(ciTestCovPayload io.Reader, format string) error
7575
GetSettingsFunc func() (*net.SettingsResponseData, error)
76-
GetEarlyFlakeDetectionDataFunc func() (*net.EfdResponseData, error)
76+
GetKnownTestsFunc func() (*net.KnownTestsResponseData, error)
7777
GetCommitsFunc func(localCommits []string) ([]string, error)
7878
SendPackFilesFunc func(commitSha string, packFiles []string) (bytes int64, err error)
7979
GetSkippableTestsFunc func() (correlationId string, skippables map[string]map[string][]net.SkippableResponseDataAttributes, err error)
@@ -91,8 +91,8 @@ func (m *MockClient) GetSettings() (*net.SettingsResponseData, error) {
9191
return m.GetSettingsFunc()
9292
}
9393

94-
func (m *MockClient) GetEarlyFlakeDetectionData() (*net.EfdResponseData, error) {
95-
return m.GetEarlyFlakeDetectionDataFunc()
94+
func (m *MockClient) GetKnownTests() (*net.KnownTestsResponseData, error) {
95+
return m.GetKnownTestsFunc()
9696
}
9797

9898
func (m *MockClient) GetCommits(localCommits []string) ([]string, error) {

internal/civisibility/integrations/gotesting/instrumentation.go

Lines changed: 86 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"fmt"
1010
"reflect"
1111
"runtime"
12-
"slices"
1312
"sync"
1413
"sync/atomic"
1514
"testing"
@@ -36,7 +35,9 @@ type (
3635
panicData any // panic data recovered from an internal test execution when using an additional feature wrapper
3736
panicStacktrace string // stacktrace from the panic recovered from an internal test
3837
isARetry bool // flag to tag if a current test execution is a retry
39-
isANewTest bool // flag to tag if a current test execution is part of a new test (EFD not known test)
38+
isANewTest bool // flag to tag if a current test execution is part of a new test
39+
isEFDExecution bool // flag to tag if a current test execution is part of an EFD execution
40+
isATRExecution bool // flag to tag if a current test execution is part of an ATR execution
4041
hasAdditionalFeatureWrapper bool // flag to check if the current execution is part of an additional feature wrapper
4142
}
4243

@@ -234,7 +235,10 @@ func applyFlakyTestRetriesAdditionalFeature(targetFunc func(*testing.T)) (func(*
234235
}
235236
}
236237
},
237-
execMetaAdjust: nil, // No execMetaAdjust needed
238+
execMetaAdjust: func(execMeta *testExecutionMetadata, executionIndex int) {
239+
// Set the flag ATR execution to true
240+
execMeta.isATRExecution = true
241+
},
238242
})
239243
}, true
240244
}
@@ -243,95 +247,82 @@ func applyFlakyTestRetriesAdditionalFeature(targetFunc func(*testing.T)) (func(*
243247

244248
// applyEarlyFlakeDetectionAdditionalFeature applies the early flake detection feature as a wrapper of a func(*testing.T)
245249
func applyEarlyFlakeDetectionAdditionalFeature(testInfo *commonInfo, targetFunc func(*testing.T), settings *net.SettingsResponseData) (func(*testing.T), bool) {
246-
earlyFlakeDetectionData := integrations.GetEarlyFlakeDetectionSettings()
247-
if earlyFlakeDetectionData != nil &&
248-
len(earlyFlakeDetectionData.Tests) > 0 {
249-
250-
// Define is a known test flag
251-
isAKnownTest := false
252-
253-
// Check if the test is a known test or a new one
254-
if knownSuites, ok := earlyFlakeDetectionData.Tests[testInfo.moduleName]; ok {
255-
if knownTests, ok := knownSuites[testInfo.suiteName]; ok {
256-
if slices.Contains(knownTests, testInfo.testName) {
257-
isAKnownTest = true
258-
}
259-
}
260-
}
250+
isKnown, hasKnownData := isKnownTest(testInfo)
251+
if !hasKnownData || isKnown {
252+
return targetFunc, false
253+
}
261254

262-
// If it's a new test, then we apply the EFD wrapper
263-
if !isAKnownTest {
264-
return func(t *testing.T) {
265-
var testPassCount, testSkipCount, testFailCount int
266-
267-
runTestWithRetry(&runTestWithRetryOptions{
268-
targetFunc: targetFunc,
269-
t: t,
270-
initialRetryCount: 0,
271-
adjustRetryCount: func(duration time.Duration) int64 {
272-
slowTestRetriesSettings := settings.EarlyFlakeDetection.SlowTestRetries
273-
durationSecs := duration.Seconds()
274-
if durationSecs < 5 {
275-
return int64(slowTestRetriesSettings.FiveS)
276-
} else if durationSecs < 10 {
277-
return int64(slowTestRetriesSettings.TenS)
278-
} else if durationSecs < 30 {
279-
return int64(slowTestRetriesSettings.ThirtyS)
280-
} else if duration.Minutes() < 5 {
281-
return int64(slowTestRetriesSettings.FiveM)
282-
}
283-
return 0
284-
},
285-
shouldRetry: func(ptrToLocalT *testing.T, executionIndex int, remainingRetries int64) bool {
286-
return remainingRetries >= 0
287-
},
288-
perExecution: func(ptrToLocalT *testing.T, executionIndex int, duration time.Duration) {
289-
// Collect test results
290-
if ptrToLocalT.Failed() {
291-
testFailCount++
292-
} else if ptrToLocalT.Skipped() {
293-
testSkipCount++
294-
} else {
295-
testPassCount++
296-
}
297-
},
298-
onRetryEnd: func(t *testing.T, executionIndex int, lastPtrToLocalT *testing.T) {
299-
// Update test status based on collected counts
300-
tCommonPrivates := getTestPrivateFields(t)
301-
if tCommonPrivates == nil {
302-
panic("getting test private fields failed")
303-
}
304-
status := "passed"
305-
if testPassCount == 0 {
306-
if testSkipCount > 0 {
307-
status = "skipped"
308-
tCommonPrivates.SetSkipped(true)
309-
}
310-
if testFailCount > 0 {
311-
status = "failed"
312-
tCommonPrivates.SetFailed(true)
313-
tParentCommonPrivates := getTestParentPrivateFields(t)
314-
if tParentCommonPrivates == nil {
315-
panic("getting test parent private fields failed")
316-
}
317-
tParentCommonPrivates.SetFailed(true)
318-
}
255+
// If it's a new test, then we apply the EFD wrapper
256+
return func(t *testing.T) {
257+
var testPassCount, testSkipCount, testFailCount int
258+
259+
runTestWithRetry(&runTestWithRetryOptions{
260+
targetFunc: targetFunc,
261+
t: t,
262+
initialRetryCount: 0,
263+
adjustRetryCount: func(duration time.Duration) int64 {
264+
slowTestRetriesSettings := settings.EarlyFlakeDetection.SlowTestRetries
265+
durationSecs := duration.Seconds()
266+
if durationSecs < 5 {
267+
return int64(slowTestRetriesSettings.FiveS)
268+
} else if durationSecs < 10 {
269+
return int64(slowTestRetriesSettings.TenS)
270+
} else if durationSecs < 30 {
271+
return int64(slowTestRetriesSettings.ThirtyS)
272+
} else if duration.Minutes() < 5 {
273+
return int64(slowTestRetriesSettings.FiveM)
274+
}
275+
return 0
276+
},
277+
shouldRetry: func(ptrToLocalT *testing.T, executionIndex int, remainingRetries int64) bool {
278+
return remainingRetries >= 0
279+
},
280+
perExecution: func(ptrToLocalT *testing.T, executionIndex int, duration time.Duration) {
281+
// Collect test results
282+
if ptrToLocalT.Failed() {
283+
testFailCount++
284+
} else if ptrToLocalT.Skipped() {
285+
testSkipCount++
286+
} else {
287+
testPassCount++
288+
}
289+
},
290+
onRetryEnd: func(t *testing.T, executionIndex int, lastPtrToLocalT *testing.T) {
291+
// Update test status based on collected counts
292+
tCommonPrivates := getTestPrivateFields(t)
293+
if tCommonPrivates == nil {
294+
panic("getting test private fields failed")
295+
}
296+
status := "passed"
297+
if testPassCount == 0 {
298+
if testSkipCount > 0 {
299+
status = "skipped"
300+
tCommonPrivates.SetSkipped(true)
301+
}
302+
if testFailCount > 0 {
303+
status = "failed"
304+
tCommonPrivates.SetFailed(true)
305+
tParentCommonPrivates := getTestParentPrivateFields(t)
306+
if tParentCommonPrivates == nil {
307+
panic("getting test parent private fields failed")
319308
}
309+
tParentCommonPrivates.SetFailed(true)
310+
}
311+
}
320312

321-
// Print summary after retries
322-
if executionIndex > 0 {
323-
fmt.Printf(" [ %v after %v retries by Datadog's early flake detection ]\n", status, executionIndex)
324-
}
325-
},
326-
execMetaAdjust: func(execMeta *testExecutionMetadata, executionIndex int) {
327-
// Set the flag new test to true
328-
execMeta.isANewTest = true
329-
},
330-
})
331-
}, true
332-
}
333-
}
334-
return targetFunc, false
313+
// Print summary after retries
314+
if executionIndex > 0 {
315+
fmt.Printf(" [ %v after %v retries by Datadog's early flake detection ]\n", status, executionIndex)
316+
}
317+
},
318+
execMetaAdjust: func(execMeta *testExecutionMetadata, executionIndex int) {
319+
// Set the flag new test to true
320+
execMeta.isANewTest = true
321+
// Set the flag EFD execution to true
322+
execMeta.isEFDExecution = true
323+
},
324+
})
325+
}, true
335326
}
336327

337328
// runTestWithRetry encapsulates the common retry logic for test functions.
@@ -386,6 +377,12 @@ func runTestWithRetry(options *runTestWithRetryOptions) {
386377
if originalExecMeta.isARetry {
387378
execMeta.isARetry = true
388379
}
380+
if originalExecMeta.isEFDExecution {
381+
execMeta.isEFDExecution = true
382+
}
383+
if originalExecMeta.isATRExecution {
384+
execMeta.isATRExecution = true
385+
}
389386
}
390387

391388
// If we are in a retry execution, set the `isARetry` flag

internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ func instrumentTestingTFunc(f func(*testing.T)) func(*testing.T) {
170170
if parentExecMeta.isARetry {
171171
execMeta.isARetry = true
172172
}
173+
if parentExecMeta.isEFDExecution {
174+
execMeta.isEFDExecution = true
175+
}
176+
if parentExecMeta.isATRExecution {
177+
execMeta.isATRExecution = true
178+
}
173179
}
174180
}
175181

@@ -186,6 +192,15 @@ func instrumentTestingTFunc(f func(*testing.T)) func(*testing.T) {
186192
if execMeta.isARetry {
187193
// Set the retry tag
188194
test.SetTag(constants.TestIsRetry, "true")
195+
196+
// If the execution is an EFD execution we tag the test event reason
197+
if execMeta.isEFDExecution {
198+
// Set the EFD as the retry reason
199+
test.SetTag(constants.TestRetryReason, "efd")
200+
} else if execMeta.isATRExecution {
201+
// Set the ATR as the retry reason
202+
test.SetTag(constants.TestRetryReason, "atr")
203+
}
189204
}
190205

191206
defer func() {

0 commit comments

Comments
 (0)