-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpattern.go
More file actions
342 lines (301 loc) · 10.5 KB
/
pattern.go
File metadata and controls
342 lines (301 loc) · 10.5 KB
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
package test
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"unicode"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tkrop/go-testing/internal/reflect"
)
// Ptr is a convenience function to obtain the pointer to the given value.
// This is particularly useful to create pointers to literal values in test
// setup code, e.g., `test.Ptr(42)` or `test.Ptr("value")`.
func Ptr[T any](v T) *T {
return &v
}
// Must is a convenience method returning the value of the first argument and
// that panics on any error in the second argument using the provided error.
// The method allows to write concise test setup code.
func Must[T any](arg T, err error) T {
if err != nil {
panic(err)
}
return arg
}
// Cast is a convenience function to cast the given argument to the specified
// type or panic if the cast fails. The method allows to write concise test
// setup code granting meaningful type checks.
func Cast[T any](arg any) T {
val, ok := arg.(T)
if !ok {
panic(fmt.Sprintf("cast failed [%T]: %v", val, arg))
}
return val
}
// First is a convenience function to return the first argument and ignore all
// others arguments. The method allows to write concise test setup code.
func First[T any](arg T, _ ...any) T { return arg }
// Recover is a convenience method to be used in deferred calls to verify that
// a panic occurred with the expected panic response. The function creates a
// test failure if no panic occurred or the panic response does not match the
// expected value.
func Recover(t Test, expect any) {
//revive:disable-next-line:defer // caller is expected to use defer.
if actual := recover(); actual != nil {
assert.Equal(t, expect, actual)
} else {
assert.Fail(t, "did not panic: %#v", expect)
}
}
// TODO: consider following convenience methods:
//
// // Check is a convenience method that returns the second argument and swallows
// // the first used to focus a test on the second.
// func Check[T any](swallowed any, check T) T {
// return check
// }
// // Ok is a convenience method to check whether the second boolean argument is
// // `true` while returning the first argument. If the boolean argument is
// // `false`, the method panics.
// func Ok[T any](result T, ok bool) T {
// if !ok {
// panic("bool not okay")
// }
// return result
// }
// MainParams provides the test parameters for testing a `main`-method.
type MainParams struct {
// Ctx is the context to control the test process, e.g. for deadlines and
// cancellations. If not set, a background context is used with unlimited
// duration.
Ctx context.Context //nolint:containedctx // only in test parameters.
// Args are the command line arguments to provide to the test process. The
// first argument is usually the program name, typically `os.Args[0]`. If
// not provided, the `main`-method is called with no arguments at all.
Args []string
// Env are the additional environment variables that are provided to the
// spawned test process. The existing environment variables are inherited
// and an additional `GO_TESTING_TEST` variable is set to select the test
// case.
Env []string
// ExitCode is the expected exit code when running the test process. If
// the exit code is not `0`, the error must be of type `*exec.ExitError`.
ExitCode int
// Error is the expected error when running the test process. This is only
// used in edge cases when the test error is not of type `*exec.ExitError`.
Error error
}
// GoTestingRunVar is the environment variable used to signal the new process
// to execute the `main` method instead of spawning a new test process.
const GoTestingRunVar = "GO_TESTING_RUN"
// Main creates a test function that runs the given `main`-method in a separate
// test process to protect the test execution from `os.Exit` calls while allowing
// to capture and check the exit code against the expectation. The following
// example demonstrates how to use this method to test a `main`-method:
//
// mainTestCases := map[string]test.MainParam{
// "with args": {
// Args: []string{"mock", "arg1", "arg2"},
// Env: []string{"VAR=value"},
// ExitCode: 0,
// },
// }
//
// func Main(t *testing.T) {
// test.Map(t, mainTestCases).Run(test.Main(main))
// }
//
// If the test process is expected to run longer than the default test timeout,
// a context with timeout can be provided to interrupt the test process in time.
// This e.g. can be done as follows using `test.First` to ignore the cancelFunc:
//
// Ctx: test.First(context.WithTimeout(context.Background(), time.Second))
func Main(main func()) func(t Test, param MainParams) {
return func(t Test, param MainParams) {
// Switch to execute main function in test process.
if name := os.Getenv(GoTestingRunVar); name != "" {
// Ensure only expected test is running.
if name == t.Name() {
os.Args = param.Args
main()
require.Fail(t, "os-exit not called")
}
// Skip unexpected tests.
return
}
// Prepare environment for the test process.
ctx := context.Background()
if param.Ctx != nil {
ctx = param.Ctx
}
// #nosec G204,G702 -- secured by calling only the dedicated test.
cmd := exec.CommandContext(ctx, os.Args[0],
"-test.run="+t.(*Context).t.Name())
// No stdout to allow propagation of coverage results.
cmd.Stdin, cmd.Stderr = os.Stdin, os.Stderr
cmd.Env = append(os.Environ(), append(param.Env,
GoTestingRunVar+"="+t.Name())...)
if err := cmd.Run(); err != nil || param.ExitCode != 0 {
errExit := &exec.ExitError{}
if errors.As(err, &errExit) {
require.Equal(t, param.ExitCode, errExit.ExitCode())
} else if err != nil {
require.Equal(t, param.Error, err)
}
}
require.Equal(t, param.ExitCode, cmd.ProcessState.ExitCode())
}
}
// DeepCopyParams provides test parameters for testing `DeepCopy*` functions
// generated by `k8s.io/code-generator/cmd/deepcopy-gen`, that unfortunately
// are part of the type system and thus should be unit tested for coverage.
type DeepCopyParams struct {
// Value is a template value used to generate random non-zero test values
// for testing the `DeepCopy*` functions.
Value any
}
// typeToTestName converts a reflect.Type into a human readable test case name.
// The name is derived from the base (non-pointer) type's CamelCase identifier
// converted to a hyphen-separated lower-case string. For unnamed types the
// string representation of the type is used as a fallback.
func typeToTestName(typ reflect.Type) string {
raw := typ.Name()
if raw == "" {
raw = typ.String()
}
runes := []rune(raw)
last := byte('-')
var b strings.Builder
for i, r := range runes {
if i > 0 && unicode.IsUpper(r) && (unicode.IsLower(runes[i-1]) ||
(i+1 < len(runes) && unicode.IsLower(runes[i+1]))) &&
(unicode.IsLetter(runes[i-1]) || unicode.IsDigit(runes[i-1])) {
b.WriteByte('-')
}
b.WriteRune(unicode.ToLower(r))
if r == ' ' {
last = byte(' ')
}
}
b.WriteByte(last)
return b.String()
}
// DeepCopyTestCases creates the test cases from a list of types to be tested
// either given as value or as nil pointer. For each type two test cases are
// created:
//
// * A non-nil random value, and
// * A nil pointer of the type.
//
// The types must implement either the `DeepCopy` or the `DeepCopyObject`
// method generated by `k8s.io/code-generator/cmd/deepcopy-gen` to succeed.
func DeepCopyTestCases(
seed int64, size, length int, args ...any,
) map[string]DeepCopyParams {
random := reflect.NewRandom(seed, size, length)
cases := make(map[string]DeepCopyParams)
for _, arg := range args {
base := reflect.TypeOf(arg)
for base.Kind() == reflect.Ptr {
base = base.Elem()
}
name := typeToTestName(base)
ptrType := reflect.PointerTo(base)
nilPtr := reflect.Zero(ptrType).Interface()
cases[name+"nil"] = DeepCopyParams{
Value: nilPtr,
}
cases[name+"value"] = DeepCopyParams{
Value: random.Random(nilPtr),
}
}
return cases
}
// DeepCopy provides a test function that tests a the `DeepCopy*` functions
// generated by `k8s.io/code-generator/cmd/deepcopy-gen` as part of the type
// system.
//
// The test function verifies that the copied value is equal to the original
// value but not the same reference. For simplicity, the function only requires
// a template to generate random non-zero test values that ensure covering all
// code paths. If this fails, you can vary the random seed, as well as the
// limits on the slice and map sizes (`size`) and string lengths (`len`).
//
// The following code shows a quick example of how to use this function in a
// tests:
//
// ```go
//
// var deepCopyTestCases = DeepCopyTestCases(42, 3, 10,
// &MyStruct{}, (*MyStruct)(nil), ...)
//
// func TestDeepCopy(t *testing.T) {
// test.Map(t, deepCopyTestCases).Run(test.DeepCopy)
// }
//
// ```
// *Note:* the test cases can also be generated inside the `TestDeepCopy`
// function.
func DeepCopy(t Test, p DeepCopyParams) {
// Given
value := p.Value
// When
result, err := deepCopy(value)
if errors.Is(err, errNoDeepCopyMethod) {
t.Fatalf("no deep copy method [%T]", value)
}
// Then
rv := reflect.ValueOf(value)
if rv.IsNil() {
assert.Nil(t, result)
return // Different pointer types cannot be equal.
}
// Wrap result in pointer if original value is pointer.
if rv.Kind() == reflect.Ptr {
// Create a new pointer to the result value.
rvr := reflect.ValueOf(result)
if rvr.Kind() != reflect.Ptr {
presult := reflect.New(rvr.Type())
presult.Elem().Set(rvr)
result = presult.Interface()
}
// Only check not Same for two pointer types.
assert.NotSame(t, value, result)
}
assert.Equal(t, value, result)
}
// deepCopy performs a deep copy of the given value using the either the
// `DeepCopyObject` or the `DeepCopy` method - in this order.
func deepCopy(value any) (any, error) {
rv := reflect.ValueOf(value)
if !rv.IsValid() {
return nil, errNoDeepCopyMethod
}
if rv.Kind() == reflect.Ptr && rv.IsNil() {
elem := rv.Type().Elem()
zeroValue := reflect.Zero(elem)
if method, ok := elem.MethodByName("DeepCopyObject"); ok {
return zeroValue.Method(method.Index).
Call(nil)[0].Interface(), nil
}
if method, ok := elem.MethodByName("DeepCopy"); ok {
return zeroValue.Method(method.Index).
Call(nil)[0].Interface(), nil
}
}
method := rv.MethodByName("DeepCopyObject")
if method.IsValid() {
return method.Call(nil)[0].Interface(), nil
}
method = rv.MethodByName("DeepCopy")
if method.IsValid() {
return method.Call(nil)[0].Interface(), nil
}
return nil, errNoDeepCopyMethod
}
// errNoDeepCopyMethod is the error returned when no deep copy method is found.
var errNoDeepCopyMethod = errors.New("no deep copy method")