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  }