about summary refs log tree commit diff
path: root/main/civisibility/integrations/gotesting/testingT.go
blob: 5b7f117735a621d7518f4bf6a56a3663784ebcdf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// 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 (
	"context"
	"fmt"
	"reflect"
	"runtime"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"ci-visibility-test-github/main/civisibility/integrations"
	"ci-visibility-test-github/main/civisibility/utils"

	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
)

var (
	// ciVisibilityTests holds a map of *testing.T to civisibility.DdTest for tracking tests.
	ciVisibilityTests = map[*testing.T]integrations.DdTest{}

	// ciVisibilityTestsMutex is a read-write mutex for synchronizing access to ciVisibilityTests.
	ciVisibilityTestsMutex sync.RWMutex
)

// T is a type alias for testing.T to provide additional methods for CI visibility.
type T testing.T

// GetTest is a helper to return *gotesting.T from *testing.T.
// Internally, it is just a (*gotesting.T)(t) cast.
func GetTest(t *testing.T) *T {
	return (*T)(t)
}

// Run runs f as a subtest of t called name. It runs f in a separate goroutine
// and blocks until f returns or calls t.Parallel to become a parallel test.
// Run reports whether f succeeded (or at least did not fail before calling t.Parallel).
//
// Run may be called simultaneously from multiple goroutines, but all such calls
// must return before the outer test function for t returns.
func (ddt *T) Run(name string, f func(*testing.T)) bool {
	// Reflect the function to obtain its pointer.
	fReflect := reflect.Indirect(reflect.ValueOf(f))
	moduleName, suiteName := utils.GetModuleAndSuiteName(fReflect.Pointer())
	originalFunc := runtime.FuncForPC(fReflect.Pointer())

	// Increment the test count in the module.
	atomic.AddInt32(modulesCounters[moduleName], 1)

	// Increment the test count in the suite.
	atomic.AddInt32(suitesCounters[suiteName], 1)

	t := (*testing.T)(ddt)
	return t.Run(name, func(t *testing.T) {
		// Create or retrieve the module, suite, and test for CI visibility.
		module := session.GetOrCreateModuleWithFramework(moduleName, testFramework, runtime.Version())
		suite := module.GetOrCreateSuite(suiteName)
		test := suite.CreateTest(t.Name())
		test.SetTestFunc(originalFunc)
		setCiVisibilityTest(t, test)
		defer func() {
			if r := recover(); r != nil {
				// Handle panic and set error information.
				test.SetErrorInfo("panic", fmt.Sprint(r), utils.GetStacktrace(1))
				test.Close(integrations.ResultStatusFail)
				checkModuleAndSuite(module, suite)
				integrations.ExitCiVisibility()
				panic(r)
			} else {
				// Normal finalization: determine the test result based on its state.
				if t.Failed() {
					test.SetTag(ext.Error, true)
					suite.SetTag(ext.Error, true)
					module.SetTag(ext.Error, true)
					test.Close(integrations.ResultStatusFail)
				} else if t.Skipped() {
					test.Close(integrations.ResultStatusSkip)
				} else {
					test.Close(integrations.ResultStatusPass)
				}
				checkModuleAndSuite(module, suite)
			}
		}()

		// Execute the original test function.
		f(t)
	})
}

// Context returns the CI Visibility context of the Test span.
// This may be used to create test's children spans useful for
// integration tests.
func (ddt *T) Context() context.Context {
	t := (*testing.T)(ddt)
	ciTest := getCiVisibilityTest(t)
	if ciTest != nil {
		return ciTest.Context()
	}

	return context.Background()
}

// Fail marks the function as having failed but continues execution.
func (ddt *T) Fail() { ddt.getTWithError("Fail", "failed test").Fail() }

// FailNow marks the function as having failed and stops its execution
// by calling runtime.Goexit (which then runs all deferred calls in the
// current goroutine). Execution will continue at the next test or benchmark.
// FailNow must be called from the goroutine running the test or benchmark function,
// not from other goroutines created during the test. Calling FailNow does not stop
// those other goroutines.
func (ddt *T) FailNow() {
	t := ddt.getTWithError("FailNow", "failed test")
	integrations.ExitCiVisibility()
	t.FailNow()
}

// Error is equivalent to Log followed by Fail.
func (ddt *T) Error(args ...any) { ddt.getTWithError("Error", fmt.Sprint(args...)).Error(args...) }

// Errorf is equivalent to Logf followed by Fail.
func (ddt *T) Errorf(format string, args ...any) {
	ddt.getTWithError("Errorf", fmt.Sprintf(format, args...)).Errorf(format, args...)
}

// Fatal is equivalent to Log followed by FailNow.
func (ddt *T) Fatal(args ...any) { ddt.getTWithError("Fatal", fmt.Sprint(args...)).Fatal(args...) }

// Fatalf is equivalent to Logf followed by FailNow.
func (ddt *T) Fatalf(format string, args ...any) {
	ddt.getTWithError("Fatalf", fmt.Sprintf(format, args...)).Fatalf(format, args...)
}

// Skip is equivalent to Log followed by SkipNow.
func (ddt *T) Skip(args ...any) { ddt.getTWithSkip(fmt.Sprint(args...)).Skip(args...) }

// Skipf is equivalent to Logf followed by SkipNow.
func (ddt *T) Skipf(format string, args ...any) {
	ddt.getTWithSkip(fmt.Sprintf(format, args...)).Skipf(format, args...)
}

// SkipNow marks the test as having been skipped and stops its execution
// by calling runtime.Goexit. If a test fails (see Error, Errorf, Fail) and is then skipped,
// it is still considered to have failed. Execution will continue at the next test or benchmark.
// SkipNow must be called from the goroutine running the test, not from other goroutines created
// during the test. Calling SkipNow does not stop those other goroutines.
func (ddt *T) SkipNow() {
	t := (*testing.T)(ddt)
	ciTest := getCiVisibilityTest(t)
	if ciTest != nil {
		ciTest.Close(integrations.ResultStatusSkip)
	}

	t.SkipNow()
}

// Parallel signals that this test is to be run in parallel with (and only with)
// other parallel tests. When a test is run multiple times due to use of
// -test.count or -test.cpu, multiple instances of a single test never run in
// parallel with each other.
func (ddt *T) Parallel() { (*testing.T)(ddt).Parallel() }

// Deadline reports the time at which the test binary will have
// exceeded the timeout specified by the -timeout flag.
// The ok result is false if the -timeout flag indicates “no timeout” (0).
func (ddt *T) Deadline() (deadline time.Time, ok bool) {
	return (*testing.T)(ddt).Deadline()
}

// Setenv calls os.Setenv(key, value) and uses Cleanup to
// restore the environment variable to its original value
// after the test. Because Setenv affects the whole process,
// it cannot be used in parallel tests or tests with parallel ancestors.
func (ddt *T) Setenv(key, value string) { (*testing.T)(ddt).Setenv(key, value) }

func (ddt *T) getTWithError(errType string, errMessage string) *testing.T {
	t := (*testing.T)(ddt)
	ciTest := getCiVisibilityTest(t)
	if ciTest != nil {
		ciTest.SetErrorInfo(errType, errMessage, utils.GetStacktrace(2))
	}
	return t
}

func (ddt *T) getTWithSkip(skipReason string) *testing.T {
	t := (*testing.T)(ddt)
	ciTest := getCiVisibilityTest(t)
	if ciTest != nil {
		ciTest.CloseWithFinishTimeAndSkipReason(integrations.ResultStatusSkip, time.Now(), skipReason)
	}
	return t
}

// getCiVisibilityTest retrieves the CI visibility test associated with a given *testing.T.
func getCiVisibilityTest(t *testing.T) integrations.DdTest {
	ciVisibilityTestsMutex.RLock()
	defer ciVisibilityTestsMutex.RUnlock()

	if v, ok := ciVisibilityTests[t]; ok {
		return v
	}

	return nil
}

// setCiVisibilityTest associates a CI visibility test with a given *testing.T.
func setCiVisibilityTest(t *testing.T, ciTest integrations.DdTest) {
	ciVisibilityTestsMutex.Lock()
	defer ciVisibilityTestsMutex.Unlock()
	ciVisibilityTests[t] = ciTest
}