-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy patht.go
More file actions
604 lines (467 loc) · 14.2 KB
/
Copy patht.go
File metadata and controls
604 lines (467 loc) · 14.2 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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
//nolint:funcorder // private and public methods close to each other for readability
package testo
import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"slices"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/ozontech/testo/internal/reflectutil"
"github.com/ozontech/testo/internal/syncutil"
"github.com/ozontech/testo/internal/testnamer"
"github.com/ozontech/testo/testoplugin"
"github.com/ozontech/testo/testoreflect"
)
type common interface {
testing.TB
Deadline() (deadline time.Time, ok bool)
Parallel()
}
// TestingT is an interface for [testing.T].
type TestingT interface {
common
Run(name string, f func(t *testing.T)) bool
}
// CommonT is the interface common for all [T] derivatives.
type CommonT interface {
common
unwrap() *T
}
type (
// T is a wrapper for [testing.T].
// This is a core entity in testo and used as a [testing.T] replacement.
//
// The common pattern is to embed it into new struct type:
//
// type MyT struct {
// *testo.T
// *SomePlugin
// }
//
// Plugins can also optionally embed it - testo will automatically initialize it
// by sharing the same value as an actual currently running test's T.
//
// type SomePlugin struct { *testo.T }
T struct {
common
// we double testing t interfaces,
// so that we still can have access for testing.T.Run
// but the user don't.
testingT TestingT
testNamer *testnamer.Namer
parent *T
spec testoplugin.Spec
// levelOptions stores options passed for the
// current level through [Run], [RunSuite] or [For].
levelOptions []testoplugin.Option
// reflection holds information for [Reflect].
reflection syncutil.Guarded[testoreflect.Reflection]
failureSource atomicInt[testoreflect.TestFailureSource]
failureKind atomicInt[testoreflect.TestFailureKind]
hasFatalSubtest atomic.Bool
// propagateParallel if enabled, will route .Parallel() calls
// to suite's TestingT.
//
// Used in [RunTest] and [Test].
propagateParallel bool
plugins map[reflect.Type]testoplugin.Plugin
}
testoT = T
)
// Plugin implements [testoplugin.Plugin].
//
// This is a placeholder to prevent other .Plugin methods being promoted.
func (*T) Plugin(testoplugin.Plugin, ...testoplugin.Option) testoplugin.Spec {
return testoplugin.Spec{}
}
// Context returns a context that is canceled just before
// Cleanup-registered functions are called.
//
// Cleanup functions can wait for any resources
// that shut down on [context.Context.Done] before the test completes.
func (t *T) Context() context.Context {
t.Helper()
return t.spec.Overrides.Context.Call(t.common.Context)()
}
// 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 (t *T) Parallel() {
t.Helper()
t.spec.Overrides.Parallel.Call(t.parallel)()
}
func (t *T) parallel() {
t.Helper()
if t.propagateParallel {
t.reflection.Load().Suite.TestingT.Parallel()
return
}
t.common.Parallel()
}
// 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 (t *T) Setenv(key, value string) {
t.Helper()
t.spec.Overrides.Setenv.Call(t.setenv)(key, value)
}
// setenv is 1:1 copy from testing.common.Setenv.
// we don't use native setenv because that way we won't use
// overrides for methods such as fatal or cleanup.
func (t *T) setenv(key, value string) {
t.Helper()
prevValue, ok := os.LookupEnv(key)
if err := os.Setenv(key, value); err != nil {
t.Fatalf("cannot set environment variable: %v", err)
}
if ok {
t.Cleanup(func() { _ = os.Setenv(key, prevValue) })
return
}
t.Cleanup(func() { _ = os.Unsetenv(key) })
}
// TempDir returns a temporary directory for the test to use.
// The directory is automatically removed when the test and
// all its subtests complete.
// Each subsequent call to t.TempDir returns a unique directory;
// if the directory creation fails, TempDir terminates the test by calling Fatal.
func (t *T) TempDir() string {
t.Helper()
return t.spec.Overrides.TempDir.Call(t.common.TempDir)()
}
// Log formats its arguments using default formatting, analogous to Println,
// and records the text in the error log. For tests, the text will be printed only if
// the test fails or the -test.v flag is set. For benchmarks, the text is always
// printed to avoid having performance depend on the value of the -test.v flag.
func (t *T) Log(args ...any) {
t.Helper()
t.spec.Overrides.Log.Call(t.common.Log)(args...)
}
// Logf formats its arguments according to the format, analogous to Printf, and
// records the text in the error log. A final newline is added if not provided. For
// tests, the text will be printed only if the test fails or the -test.v flag is
// set. For benchmarks, the text is always printed to avoid having performance
// depend on the value of the -test.v flag.
func (t *T) Logf(format string, args ...any) {
t.Helper()
t.Log(fmt.Sprintf(format, args...))
}
// Deadline reports the time at which the test binary will have
// exceeded the timeout specified by the -timeout flag.
//
// By default, the ok result is false if the -timeout flag indicates "no timeout" (0).
func (t *T) Deadline() (time.Time, bool) {
t.Helper()
return t.spec.Overrides.Deadline.Call(t.common.Deadline)()
}
// Errorf is equivalent to Error with formatted message.
func (t *T) Errorf(format string, args ...any) {
t.Helper()
t.Error(fmt.Sprintf(format, args...))
}
// Error is equivalent to Log followed by Fail.
func (t *T) Error(args ...any) {
t.Helper()
t.spec.Overrides.Error.Call(t.error)(args...)
}
func (t *T) error(args ...any) {
t.Helper()
t.Log(args...)
t.Fail()
}
// Skip is equivalent to Log followed by SkipNow.
func (t *T) Skip(args ...any) {
t.Helper()
t.spec.Overrides.Skip.Call(t.skip)(args...)
}
func (t *T) skip(args ...any) {
t.Helper()
t.Log(args...)
t.SkipNow()
}
// 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. See also FailNow.
// 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 (t *T) SkipNow() {
t.Helper()
t.spec.Overrides.SkipNow.Call(t.common.SkipNow)()
}
// Skipf is equivalent to Skip with formatted message.
func (t *T) Skipf(format string, args ...any) {
t.Helper()
t.Skip(fmt.Sprintf(format, args...))
}
// Skipped reports whether the test was skipped.
func (t *T) Skipped() bool {
t.Helper()
return t.spec.Overrides.Skipped.Call(t.common.Skipped)()
}
// Fail marks the function as having failed but continues execution.
func (t *T) Fail() {
t.Helper()
t.spec.Overrides.Fail.Call(t.fail)()
}
func (t *T) fail() {
t.Helper()
t.markFailure(testoreflect.TestFailureKindSoft)
t.common.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 (t *T) FailNow() {
t.Helper()
t.spec.Overrides.FailNow.Call(t.failNow)()
}
func (t *T) failNow() {
t.Helper()
t.markFailure(testoreflect.TestFailureKindFatal)
t.common.FailNow()
}
// Failed reports whether the function has failed.
func (t *T) Failed() bool {
t.Helper()
return t.spec.Overrides.Failed.Call(t.common.Failed)()
}
// Fatal is equivalent to Log followed by FailNow.
func (t *T) Fatal(args ...any) {
t.Helper()
t.spec.Overrides.Fatal.Call(t.fatal)(args...)
}
func (t *T) fatal(args ...any) {
t.Helper()
t.Log(args...)
t.FailNow()
}
// Fatalf is equivalent to Fatal with formatted message.
func (t *T) Fatalf(format string, args ...any) {
t.Helper()
t.Fatal(fmt.Sprintf(format, args...))
}
// Chdir calls [os.Chdir] and uses Cleanup to restore the current
// working directory to its original value after the test. On Unix, it
// also sets PWD environment variable for the duration of the test.
//
// Because Chdir affects the whole process, it cannot be used
// in parallel tests or tests with parallel ancestors.
func (t *T) Chdir(dir string) {
t.Helper()
t.spec.Overrides.Chdir.Call(t.chdir)(dir)
}
// chdir is 1:1 copy from testing.common.Chdir.
// we don't use native chdir because that way we won't use
// overrides for methods such as fatal or cleanup.
func (t *T) chdir(dir string) {
t.Helper()
oldwd, err := os.Open(".")
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
// On POSIX platforms, PWD represents "an absolute pathname of the
// current working directory." Since we are changing the working
// directory, we should also set or update PWD to reflect that.
switch runtime.GOOS {
case "windows", "plan9":
// Windows and Plan 9 do not use the PWD variable.
default:
if !filepath.IsAbs(dir) {
dir, err = os.Getwd()
if err != nil {
t.Fatal(err)
}
}
t.Setenv("PWD", dir)
}
t.Cleanup(func() {
err := oldwd.Chdir()
_ = oldwd.Close()
if err != nil {
// It's not safe to continue with tests if we can't
// get back to the original working directory. Since
// we are holding a dirfd, this is highly unlikely.
panic("testo.Chdir: " + err.Error())
}
})
}
// Cleanup registers a function to be called when the test (or subtest) and all its
// subtests complete. Cleanup functions will be called in last added,
// first called order.
func (t *T) Cleanup(f func()) {
t.Helper()
t.spec.Overrides.Cleanup.Call(t.common.Cleanup)(f)
}
// Name returns the name of the running (sub-) test or benchmark.
//
// The name will include the name of the test along with the names of
// any nested sub-tests. If two sibling sub-tests have the same name,
// Name will append a suffix to guarantee the returned name is unique.
func (t *T) Name() string {
t.Helper()
return t.reflection.Load().Test.GetName()
}
// unwrap the underlying T.
//
// It works since T's are embedded in user-defined structs.
func (t *T) unwrap() *T {
return t
}
// level indicates how deep this t is.
// That is, it shows the number of parents it has and zero if none.
func (t *T) level() int {
var level int
parent := t.parent
for parent != nil {
level++
parent = parent.parent
}
return level
}
// markFailure sets failure kind to the current t
// and promotes it for all ancestors.
func (t *T) markFailure(kind testoreflect.TestFailureKind) {
if kind == testoreflect.TestFailureKindNone {
return
}
t.failureKind.Store(kind)
t.failureSource.Store(testoreflect.TestFailureSourceSelf)
parent := t.parent
for parent != nil {
// parent may already have fatal failure,
// so we overwrite parent failure kind only if it has none.
parent.failureKind.CompareAndSwap(
testoreflect.TestFailureKindNone,
testoreflect.TestFailureKindSoft,
)
parent.failureSource.CompareAndSwap(
testoreflect.TestFailureSourceNone,
testoreflect.TestFailureSourceChild,
)
if kind == testoreflect.TestFailureKindFatal {
parent.hasFatalSubtest.Store(true)
}
parent = parent.parent
}
}
func (t *T) options() []testoplugin.Option {
size := len(t.levelOptions)
byLevel := [][]testoplugin.Option{t.levelOptions}
parent := t.parent
for parent != nil {
level := make([]testoplugin.Option, 0, len(parent.levelOptions))
for _, o := range parent.levelOptions {
if o.Propagate {
level = append(level, o)
}
}
byLevel = append(byLevel, level)
size += len(level)
parent = parent.parent
}
options := make([]testoplugin.Option, 0, size)
// so that child options come after parent options
for _, level := range slices.Backward(byLevel) {
options = append(options, level...)
}
return options
}
func (t *T) pluginNames() []string {
names := make([]string, 0, len(t.unwrap().plugins))
for typ := range t.unwrap().plugins {
if typ == reflect.TypeFor[*T]() {
continue
}
names = append(names, reflectutil.Elem(typ).String())
}
slices.Sort(names)
return names
}
func (t *T) logPlugins() {
t.Helper()
names := t.unwrap().pluginNames()
if len(names) == 0 {
return
}
t.testingT.Logf(
"testo: plugins collected: %d: %s\n",
len(names),
strings.Join(names, ", "),
)
}
// Reflect returns meta information about given t.
//
// You can reflect over any test by accessing its T instance:
//
// func (Suite) TestFoo(t T) {
// r := testo.Reflect(t)
// // r stores Reflection struct.
// }
//
// Same logic applies for plugins.
// If a plugin embeds `*testo.T` it can call the same [testo.Reflect] function:
//
// type Plugin struct{ *testo.T }
//
// func (p *Plugin) Plugin(parent testoplugin.Plugin, options ...testoplugin.Options) testoplugin.Spec {
// return testoplugin.Spec{
// Hooks: testoplugin.Hooks{
// BeforeEach: testoplugin.Hook{
// Func: func() { testo.Reflect(p) }
// }
// }
// }
// }
func Reflect(t CommonT) testoreflect.Reflection {
t.Helper()
internal := t.unwrap()
info := internal.reflection.Load()
info.FailureSource = internal.failureSource.Load()
info.FailureKind = internal.failureKind.Load()
info.HasFatalSubtest = internal.hasFatalSubtest.Load()
info.TestingT = internal.testingT
return info
}
type atomicInt[T ~int | ~int8 | ~int32 | ~int64] atomic.Int64
func (a *atomicInt[T]) Load() T {
return T((*atomic.Int64)(a).Load())
}
func (a *atomicInt[T]) Store(value T) {
(*atomic.Int64)(a).Store(int64(value))
}
func (a *atomicInt[T]) CompareAndSwap(oldvalue, newvalue T) bool {
return (*atomic.Int64)(a).CompareAndSwap(int64(oldvalue), int64(newvalue))
}
func warnf(tb testing.TB, f string, args ...any) {
tb.Helper()
warn(tb, fmt.Sprintf(f, args...))
}
func warn(tb testing.TB, args ...any) {
tb.Helper()
const prefix = "testo: "
if *flagStrict {
tb.Fatal(prefix + fmt.Sprint(args...))
}
tb.Log(prefix + "warning: " + fmt.Sprint(args...))
}