go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/testutil/suite.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package testutil
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"os"
    14  	"testing"
    15  
    16  	"go.charczuk.com/sdk/db"
    17  )
    18  
    19  // FailureCodes
    20  const (
    21  	SuiteFailureTests  = 1
    22  	SuiteFailureBefore = 2
    23  	SuiteFailureAfter  = 3
    24  )
    25  
    26  // New returns a new test suite.
    27  func New(m *testing.M, opts ...Option) *Suite {
    28  	s := Suite{
    29  		M: m,
    30  	}
    31  	for _, opt := range opts {
    32  		opt(&s)
    33  	}
    34  	return &s
    35  }
    36  
    37  // Option is a mutator for a test suite.
    38  type Option func(*Suite)
    39  
    40  // OptAfter appends after run actions.
    41  func OptAfter(steps ...SuiteAction) Option {
    42  	return func(s *Suite) {
    43  		s.After = append(s.After, steps...)
    44  	}
    45  }
    46  
    47  // OptBefore appends before run actions.
    48  func OptBefore(steps ...SuiteAction) Option {
    49  	return func(s *Suite) {
    50  		s.Before = append(s.Before, steps...)
    51  	}
    52  }
    53  
    54  // OptWithDefaultDB runs a test suite with a dedicated database connection.
    55  func OptWithDefaultDB(opts ...db.Option) Option {
    56  	return func(s *Suite) {
    57  		var err error
    58  		s.Before = append(s.Before, func(ctx context.Context) error {
    59  			_defaultDB, err = CreateTestDatabase(ctx, opts...)
    60  			if err != nil {
    61  				return err
    62  			}
    63  			return nil
    64  		})
    65  		s.After = append(s.After, func(ctx context.Context) error {
    66  			if err := _defaultDB.Close(); err != nil {
    67  				return err
    68  			}
    69  			return DropTestDatabase(ctx, _defaultDB)
    70  		})
    71  	}
    72  }
    73  
    74  // SuiteAction is a step that can be run either before or after package tests.
    75  type SuiteAction func(context.Context) error
    76  
    77  // Suite is a set of before and after actions for a given package tests.
    78  type Suite struct {
    79  	M      *testing.M
    80  	Before []SuiteAction
    81  	After  []SuiteAction
    82  }
    83  
    84  // Run runs tests and calls os.Exit(...) with the exit code.
    85  func (s Suite) Run() {
    86  	os.Exit(s.RunCode())
    87  }
    88  
    89  // RunCode runs the suite and returns an exit code.
    90  //
    91  // It is used by `.Run()`, which will os.Exit(...) this code.
    92  func (s Suite) RunCode() (code int) {
    93  	ctx := context.Background()
    94  	var err error
    95  	for _, before := range s.Before {
    96  		if err = executeSafe(ctx, before); err != nil {
    97  			fmt.Fprintf(os.Stderr, "failed to complete before test actions: %+v\n", err)
    98  			code = SuiteFailureBefore
    99  			return
   100  		}
   101  	}
   102  	defer func() {
   103  		for _, after := range s.After {
   104  			if err = executeSafe(ctx, after); err != nil {
   105  				fmt.Fprintf(os.Stderr, "failed to complete after test actions: %+v\n", err)
   106  				code = SuiteFailureAfter
   107  				return
   108  			}
   109  		}
   110  	}()
   111  	if s.M != nil {
   112  		code = s.M.Run()
   113  	}
   114  	return
   115  }
   116  
   117  func executeSafe(ctx context.Context, action func(context.Context) error) (err error) {
   118  	defer func() {
   119  		if r := recover(); r != nil {
   120  			err = fmt.Errorf("%v", r)
   121  		}
   122  	}()
   123  	err = action(ctx)
   124  	return
   125  }