github.com/mailgun/holster/v4@v4.20.0/functional/t.go (about) 1 /* 2 Copyright 2022 Mailgun Technologies Inc 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package functional 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "os" 24 "reflect" 25 "runtime" 26 "runtime/debug" 27 "strings" 28 "time" 29 30 "github.com/mailgun/holster/v4/errors" 31 ) 32 33 // Functional test context. 34 type T struct { 35 name string 36 ctx context.Context 37 deadline time.Time 38 indent int 39 writer io.Writer 40 errWriter io.Writer 41 args []string 42 result TestResult 43 } 44 45 type TestResult struct { 46 Pass bool 47 Skipped bool 48 StartTime time.Time 49 EndTime time.Time 50 } 51 52 // Functional test code. 53 type TestFunc func(t *T) 54 55 var maxTimeout = 10 * time.Minute 56 57 func newT(name string, opts ...FunctionalOption) *T { 58 t := &T{ 59 name: name, 60 writer: os.Stdout, 61 errWriter: os.Stderr, 62 } 63 64 for _, opt := range opts { 65 opt.Apply(t) 66 } 67 68 return t 69 } 70 71 func (t *T) Name() string { 72 return t.name 73 } 74 75 func (t *T) Run(name string, fn TestFunc) bool { 76 t2 := &T{ 77 name: joinName(t.name, name), 78 indent: t.indent + 1, 79 writer: t.writer, 80 errWriter: t.errWriter, 81 } 82 83 t2.invoke(t.ctx, fn) 84 85 if !t2.result.Pass { 86 t.result.Pass = false 87 } 88 89 return t.result.Pass 90 } 91 92 func (t *T) Deadline() (time.Time, error) { 93 if t.deadline.IsZero() { 94 return time.Time{}, errors.New("Deadline not set") 95 } 96 return t.deadline, nil 97 } 98 99 func (t *T) Error(args ...any) { 100 fmt.Fprintln(t.errWriter, args...) 101 t.result.Pass = false 102 } 103 104 func (t *T) Errorf(format string, args ...any) { 105 fmt.Fprintf(t.errWriter, format+"\n", args...) 106 t.result.Pass = false 107 } 108 109 func (t *T) FailNow() { 110 panic("") 111 } 112 113 func (t *T) Log(message ...any) { 114 if len(message) > 0 { 115 fmt.Fprintln(t.writer, message...) 116 } 117 } 118 119 func (t *T) Logf(format string, args ...any) { 120 fmt.Fprintf(t.writer, format+"\n", args...) 121 } 122 123 func (t *T) Args() []string { 124 return t.args 125 } 126 127 func (t *T) Skip(args ...any) { 128 t.Log(args...) 129 t.SkipNow() 130 } 131 132 func (t *T) Skipf(format string, args ...any) { 133 t.Logf(format, args...) 134 t.SkipNow() 135 } 136 137 func (t *T) Skipped() bool { 138 return t.result.Skipped 139 } 140 141 func (t *T) SkipNow() { 142 t.result.Skipped = true 143 runtime.Goexit() 144 } 145 146 func (t *T) invoke(ctx context.Context, fn TestFunc) { 147 callFn := func() { 148 fn(t) 149 } 150 t.commonInvoke(ctx, callFn, nil) 151 } 152 153 func (t *T) commonInvoke(ctx context.Context, fn, postHandler func()) { 154 if ctx.Err() != nil { 155 panic(ctx.Err()) 156 } 157 158 t.deadline = time.Now().Add(maxTimeout) 159 ctx, cancel := context.WithDeadline(ctx, t.deadline) 160 defer cancel() 161 t.ctx = ctx 162 t.result.Pass = true 163 t.Logf("≈≈≈ RUN %s", t.name) 164 t.result.StartTime = time.Now() 165 166 // Call test in goroutine. 167 done := make(chan any) 168 go func() { 169 var finished bool 170 defer func() { 171 t.result.Skipped = !finished 172 done <- recover() 173 }() 174 175 fn() 176 finished = true 177 }() 178 179 // Wait, then handle panic. 180 if fnErr := <-done; fnErr != nil { 181 errMsg := fmt.Sprintf("%v", fnErr) 182 if errMsg != "" { 183 t.Error(errMsg) 184 } 185 t.Error(debug.Stack()) 186 } 187 188 t.result.EndTime = time.Now() 189 elapsed := t.result.EndTime.Sub(t.result.StartTime) 190 191 if postHandler != nil { 192 postHandler() 193 } 194 195 switch { 196 case t.result.Skipped: 197 t.Logf("⁓⁓⁓ SKIP: %s (%s)", t.name, elapsed) 198 case t.result.Pass: 199 t.Logf("⁓⁓⁓ PASS: %s (%s)", t.name, elapsed) 200 default: 201 t.Logf("⁓⁓⁓ FAIL: %s (%s)", t.name, elapsed) 202 } 203 } 204 205 // Get base name of function. 206 func funcName(fn any) string { 207 name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() 208 idx := strings.LastIndex(name, ".") 209 if idx < 0 { 210 return name 211 } 212 return name[idx+1:] 213 } 214 215 func joinName(a, b string) string { 216 return a + "/" + b 217 }