-
Notifications
You must be signed in to change notification settings - Fork 441
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[CI Visibility] Manual Api and Go/Testing integration (#2742)
Co-authored-by: liashenko <[email protected]>
- Loading branch information
1 parent
6d9882b
commit 93cfbc8
Showing
14 changed files
with
3,036 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2024 Datadog, Inc. | ||
|
||
package integrations | ||
|
||
import ( | ||
"os" | ||
"os/signal" | ||
"regexp" | ||
"strings" | ||
"sync" | ||
"syscall" | ||
|
||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" | ||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" | ||
"gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" | ||
"gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" | ||
) | ||
|
||
// ciVisibilityCloseAction defines an action to be executed when CI visibility is closing. | ||
type ciVisibilityCloseAction func() | ||
|
||
var ( | ||
// ciVisibilityInitializationOnce ensures we initialize the CI visibility tracer only once. | ||
ciVisibilityInitializationOnce sync.Once | ||
|
||
// closeActions holds CI visibility close actions. | ||
closeActions []ciVisibilityCloseAction | ||
|
||
// closeActionsMutex synchronizes access to closeActions. | ||
closeActionsMutex sync.Mutex | ||
|
||
// mTracer contains the mock tracer instance for testing purposes | ||
mTracer mocktracer.Tracer | ||
) | ||
|
||
// EnsureCiVisibilityInitialization initializes the CI visibility tracer if it hasn't been initialized already. | ||
func EnsureCiVisibilityInitialization() { | ||
internalCiVisibilityInitialization(func(opts []tracer.StartOption) { | ||
// Initialize the tracer. | ||
tracer.Start(opts...) | ||
}) | ||
} | ||
|
||
// InitializeCIVisibilityMock initialize the mocktracer for CI Visibility usage | ||
func InitializeCIVisibilityMock() mocktracer.Tracer { | ||
internalCiVisibilityInitialization(func([]tracer.StartOption) { | ||
// Initialize the mocktracer | ||
mTracer = mocktracer.Start() | ||
}) | ||
return mTracer | ||
} | ||
|
||
func internalCiVisibilityInitialization(tracerInitializer func([]tracer.StartOption)) { | ||
ciVisibilityInitializationOnce.Do(func() { | ||
// Since calling this method indicates we are in CI Visibility mode, set the environment variable. | ||
_ = os.Setenv(constants.CiVisibilityEnabledEnvironmentVariable, "1") | ||
Check failure on line 59 in internal/civisibility/integrations/civisibility.go GitHub Actions / go get -u smoke test
|
||
|
||
// Avoid sampling rate warning (in CI Visibility mode we send all data) | ||
_ = os.Setenv("DD_TRACE_SAMPLE_RATE", "1") | ||
|
||
// Preload the CodeOwner file | ||
_ = utils.GetCodeOwners() | ||
|
||
// Preload all CI, Git, and CodeOwners tags. | ||
ciTags := utils.GetCiTags() | ||
Check failure on line 68 in internal/civisibility/integrations/civisibility.go GitHub Actions / go get -u smoke test
|
||
|
||
// Check if DD_SERVICE has been set; otherwise default to the repo name (from the spec). | ||
var opts []tracer.StartOption | ||
if v := os.Getenv("DD_SERVICE"); v == "" { | ||
if repoURL, ok := ciTags[constants.GitRepositoryURL]; ok { | ||
// regex to sanitize the repository url to be used as a service name | ||
repoRegex := regexp.MustCompile(`(?m)/([a-zA-Z0-9\\\-_.]*)$`) | ||
matches := repoRegex.FindStringSubmatch(repoURL) | ||
if len(matches) > 1 { | ||
repoURL = strings.TrimSuffix(matches[1], ".git") | ||
} | ||
opts = append(opts, tracer.WithService(repoURL)) | ||
} | ||
} | ||
|
||
// Initialize the tracer | ||
tracerInitializer(opts) | ||
|
||
// Handle SIGINT and SIGTERM signals to ensure we close all open spans and flush the tracer before exiting | ||
signals := make(chan os.Signal, 1) | ||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) | ||
go func() { | ||
<-signals | ||
ExitCiVisibility() | ||
os.Exit(1) | ||
}() | ||
}) | ||
} | ||
|
||
// PushCiVisibilityCloseAction adds a close action to be executed when CI visibility exits. | ||
func PushCiVisibilityCloseAction(action ciVisibilityCloseAction) { | ||
closeActionsMutex.Lock() | ||
defer closeActionsMutex.Unlock() | ||
closeActions = append([]ciVisibilityCloseAction{action}, closeActions...) | ||
} | ||
|
||
// ExitCiVisibility executes all registered close actions and stops the tracer. | ||
func ExitCiVisibility() { | ||
closeActionsMutex.Lock() | ||
defer closeActionsMutex.Unlock() | ||
defer func() { | ||
closeActions = []ciVisibilityCloseAction{} | ||
|
||
tracer.Flush() | ||
tracer.Stop() | ||
}() | ||
for _, v := range closeActions { | ||
v() | ||
} | ||
} |
103 changes: 103 additions & 0 deletions
103
internal/civisibility/integrations/gotesting/reflections.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2024 Datadog, Inc. | ||
|
||
package gotesting | ||
|
||
import ( | ||
"errors" | ||
"reflect" | ||
"sync" | ||
"testing" | ||
"unsafe" | ||
) | ||
|
||
// getFieldPointerFrom gets an unsafe.Pointer (gc-safe type of pointer) to a struct field | ||
// useful to get or set values to private field | ||
func getFieldPointerFrom(value any, fieldName string) (unsafe.Pointer, error) { | ||
indirectValue := reflect.Indirect(reflect.ValueOf(value)) | ||
member := indirectValue.FieldByName(fieldName) | ||
if member.IsValid() { | ||
return unsafe.Pointer(member.UnsafeAddr()), nil | ||
} | ||
|
||
return unsafe.Pointer(nil), errors.New("member is invalid") | ||
} | ||
|
||
// TESTING | ||
|
||
// getInternalTestArray gets the pointer to the testing.InternalTest array inside a | ||
// testing.M instance containing all the "root" tests | ||
func getInternalTestArray(m *testing.M) *[]testing.InternalTest { | ||
if ptr, err := getFieldPointerFrom(m, "tests"); err == nil { | ||
return (*[]testing.InternalTest)(ptr) | ||
} | ||
return nil | ||
} | ||
|
||
// BENCHMARKS | ||
|
||
// get the pointer to the internal benchmark array | ||
// getInternalBenchmarkArray gets the pointer to the testing.InternalBenchmark array inside | ||
// a testing.M instance containing all the "root" benchmarks | ||
func getInternalBenchmarkArray(m *testing.M) *[]testing.InternalBenchmark { | ||
if ptr, err := getFieldPointerFrom(m, "benchmarks"); err == nil { | ||
return (*[]testing.InternalBenchmark)(ptr) | ||
} | ||
return nil | ||
} | ||
|
||
// commonPrivateFields is collection of required private fields from testing.common | ||
type commonPrivateFields struct { | ||
mu *sync.RWMutex | ||
level *int | ||
name *string // Name of test or benchmark. | ||
} | ||
|
||
// AddLevel increase or decrease the testing.common.level field value, used by | ||
// testing.B to create the name of the benchmark test | ||
func (c *commonPrivateFields) AddLevel(delta int) int { | ||
c.mu.Lock() | ||
defer c.mu.Unlock() | ||
*c.level = *c.level + delta | ||
return *c.level | ||
} | ||
|
||
// benchmarkPrivateFields is a collection of required private fields from testing.B | ||
// also contains a pointer to the original testing.B for easy access | ||
type benchmarkPrivateFields struct { | ||
commonPrivateFields | ||
B *testing.B | ||
benchFunc *func(b *testing.B) | ||
result *testing.BenchmarkResult | ||
} | ||
|
||
// getBenchmarkPrivateFields is a method to retrieve all required privates field from | ||
// testing.B, returning a benchmarkPrivateFields instance | ||
func getBenchmarkPrivateFields(b *testing.B) *benchmarkPrivateFields { | ||
benchFields := &benchmarkPrivateFields{ | ||
B: b, | ||
} | ||
|
||
// common | ||
if ptr, err := getFieldPointerFrom(b, "mu"); err == nil { | ||
benchFields.mu = (*sync.RWMutex)(ptr) | ||
} | ||
if ptr, err := getFieldPointerFrom(b, "level"); err == nil { | ||
benchFields.level = (*int)(ptr) | ||
} | ||
if ptr, err := getFieldPointerFrom(b, "name"); err == nil { | ||
benchFields.name = (*string)(ptr) | ||
} | ||
|
||
// benchmark | ||
if ptr, err := getFieldPointerFrom(b, "benchFunc"); err == nil { | ||
benchFields.benchFunc = (*func(b *testing.B))(ptr) | ||
} | ||
if ptr, err := getFieldPointerFrom(b, "result"); err == nil { | ||
benchFields.result = (*testing.BenchmarkResult)(ptr) | ||
} | ||
|
||
return benchFields | ||
} |
150 changes: 150 additions & 0 deletions
150
internal/civisibility/integrations/gotesting/reflections_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2024 Datadog, Inc. | ||
|
||
package gotesting | ||
|
||
import ( | ||
"sync" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
// TestGetFieldPointerFrom tests the getFieldPointerFrom function. | ||
func TestGetFieldPointerFrom(t *testing.T) { | ||
// Create a mock struct with a private field | ||
mockStruct := struct { | ||
privateField string | ||
}{ | ||
privateField: "testValue", | ||
} | ||
|
||
// Attempt to get a pointer to the private field | ||
ptr, err := getFieldPointerFrom(&mockStruct, "privateField") | ||
if err != nil { | ||
t.Fatalf("Expected no error, got %v", err) | ||
} | ||
|
||
if ptr == nil { | ||
t.Fatal("Expected a valid pointer, got nil") | ||
} | ||
|
||
// Dereference the pointer to get the actual value | ||
actualValue := (*string)(ptr) | ||
if *actualValue != mockStruct.privateField { | ||
t.Fatalf("Expected 'testValue', got %s", *actualValue) | ||
} | ||
|
||
// Modify the value through the pointer | ||
*actualValue = "modified value" | ||
if *actualValue != mockStruct.privateField { | ||
t.Fatalf("Expected 'modified value', got %s", mockStruct.privateField) | ||
} | ||
|
||
// Attempt to get a pointer to a non-existent field | ||
_, err = getFieldPointerFrom(&mockStruct, "nonExistentField") | ||
if err == nil { | ||
t.Fatal("Expected an error for non-existent field, got nil") | ||
} | ||
} | ||
|
||
// TestGetInternalTestArray tests the getInternalTestArray function. | ||
func TestGetInternalTestArray(t *testing.T) { | ||
assert := assert.New(t) | ||
|
||
// Get the internal test array from the mock testing.M | ||
tests := getInternalTestArray(currentM) | ||
assert.NotNil(tests) | ||
|
||
// Check that the test array contains the expected test | ||
var testNames []string | ||
for _, v := range *tests { | ||
testNames = append(testNames, v.Name) | ||
assert.NotNil(v.F) | ||
} | ||
|
||
assert.Contains(testNames, "TestGetFieldPointerFrom") | ||
assert.Contains(testNames, "TestGetInternalTestArray") | ||
assert.Contains(testNames, "TestGetInternalBenchmarkArray") | ||
assert.Contains(testNames, "TestCommonPrivateFields_AddLevel") | ||
assert.Contains(testNames, "TestGetBenchmarkPrivateFields") | ||
} | ||
|
||
// TestGetInternalBenchmarkArray tests the getInternalBenchmarkArray function. | ||
func TestGetInternalBenchmarkArray(t *testing.T) { | ||
assert := assert.New(t) | ||
|
||
// Get the internal benchmark array from the mock testing.M | ||
benchmarks := getInternalBenchmarkArray(currentM) | ||
assert.NotNil(benchmarks) | ||
|
||
// Check that the benchmark array contains the expected benchmark | ||
var testNames []string | ||
for _, v := range *benchmarks { | ||
testNames = append(testNames, v.Name) | ||
assert.NotNil(v.F) | ||
} | ||
|
||
assert.Contains(testNames, "BenchmarkDummy") | ||
} | ||
|
||
// TestCommonPrivateFields_AddLevel tests the AddLevel method of commonPrivateFields. | ||
func TestCommonPrivateFields_AddLevel(t *testing.T) { | ||
// Create a commonPrivateFields struct with a mutex and a level | ||
level := 1 | ||
commonFields := &commonPrivateFields{ | ||
mu: &sync.RWMutex{}, | ||
level: &level, | ||
} | ||
|
||
// Add a level and check the new level | ||
newLevel := commonFields.AddLevel(1) | ||
if newLevel != 2 || newLevel != *commonFields.level { | ||
t.Fatalf("Expected level to be 2, got %d", newLevel) | ||
} | ||
|
||
// Subtract a level and check the new level | ||
newLevel = commonFields.AddLevel(-1) | ||
if newLevel != 1 || newLevel != *commonFields.level { | ||
t.Fatalf("Expected level to be 1, got %d", newLevel) | ||
} | ||
} | ||
|
||
// TestGetBenchmarkPrivateFields tests the getBenchmarkPrivateFields function. | ||
func TestGetBenchmarkPrivateFields(t *testing.T) { | ||
// Create a new testing.B instance | ||
b := &testing.B{} | ||
|
||
// Get the private fields of the benchmark | ||
benchFields := getBenchmarkPrivateFields(b) | ||
if benchFields == nil { | ||
t.Fatal("Expected a valid benchmarkPrivateFields, got nil") | ||
} | ||
|
||
// Set values to the private fields | ||
*benchFields.name = "BenchmarkTest" | ||
*benchFields.level = 1 | ||
*benchFields.benchFunc = func(b *testing.B) {} | ||
*benchFields.result = testing.BenchmarkResult{} | ||
|
||
// Check that the private fields have the expected values | ||
if benchFields.level == nil || *benchFields.level != 1 { | ||
t.Fatalf("Expected level to be 1, got %v", *benchFields.level) | ||
} | ||
|
||
if benchFields.name == nil || *benchFields.name != b.Name() { | ||
t.Fatalf("Expected name to be 'BenchmarkTest', got %v", *benchFields.name) | ||
} | ||
|
||
if benchFields.benchFunc == nil { | ||
t.Fatal("Expected benchFunc to be set, got nil") | ||
} | ||
|
||
if benchFields.result == nil { | ||
t.Fatal("Expected result to be set, got nil") | ||
} | ||
} | ||
|
||
func BenchmarkDummy(*testing.B) {} |
Oops, something went wrong.