github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/test/test_framework.go (about)

     1  package test
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"math/rand"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"runtime"
    12  	"runtime/debug"
    13  	"strings"
    14  	"sync"
    15  	"syscall"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/tickoalcantara12/micro/v3/client/cli/namespace"
    20  	"github.com/tickoalcantara12/micro/v3/util/user"
    21  )
    22  
    23  const (
    24  	minPort = 8000
    25  	maxPort = 60000
    26  )
    27  
    28  var (
    29  	retryCount        = 1
    30  	isParallel        = true
    31  	ignoreThisError   = errors.New("Do not use this error")
    32  	errFatal          = errors.New("Fatal error")
    33  	testFilter        = []string{}
    34  	maxTimeMultiplier = 1
    35  )
    36  
    37  type cmdFunc func() ([]byte, error)
    38  
    39  // Server is a micro server
    40  type Server interface {
    41  	// Run the server
    42  	Run() error
    43  	// Close shuts down the server
    44  	Close()
    45  	// Command provides a `micro` command for the server
    46  	Command() *Command
    47  	// Name of the environment
    48  	Env() string
    49  	// APIPort is the port the api is exposed on
    50  	APIPort() int
    51  	// PoxyPort is the port the proxy is exposed on
    52  	ProxyPort() int
    53  }
    54  
    55  type Command struct {
    56  	Env    string
    57  	Config string
    58  	Dir    string
    59  
    60  	sync.Mutex
    61  	// in the event an async command is run
    62  	cmd       *exec.Cmd
    63  	cmdOutput bytes.Buffer
    64  
    65  	// internal logging use
    66  	t *T
    67  }
    68  
    69  func (c *Command) args(a ...string) []string {
    70  	arguments := []string{}
    71  
    72  	// disable jwt creds which are injected so the server can run
    73  	// but shouldn't be passed to the CLI
    74  	arguments = append(arguments, "-auth_public_key", "")
    75  	arguments = append(arguments, "-auth_private_key", "")
    76  
    77  	// add config flag
    78  	arguments = append(arguments, "-c", c.Config)
    79  
    80  	// add env flag if not env command
    81  	if v := len(a); v > 0 && a[0] != "env" {
    82  		arguments = append(arguments, "-e", c.Env)
    83  	}
    84  
    85  	return append(arguments, a...)
    86  }
    87  
    88  // Exec executes a command inline
    89  func (c *Command) Exec(args ...string) ([]byte, error) {
    90  	arguments := c.args(args...)
    91  	// exec the command
    92  	// c.t.Logf("Executing command: micro %s\n", strings.Join(arguments, " "))
    93  	com := exec.Command("micro", arguments...)
    94  	if len(c.Dir) > 0 {
    95  		com.Dir = c.Dir
    96  	}
    97  	return com.CombinedOutput()
    98  }
    99  
   100  // Starts a new command
   101  func (c *Command) Start(args ...string) error {
   102  	c.Lock()
   103  	defer c.Unlock()
   104  
   105  	if c.cmd != nil {
   106  		return errors.New("command is already running")
   107  	}
   108  
   109  	arguments := c.args(args...)
   110  	c.cmd = exec.Command("micro", arguments...)
   111  
   112  	c.cmd.Stdout = &c.cmdOutput
   113  	c.cmd.Stderr = &c.cmdOutput
   114  
   115  	return c.cmd.Start()
   116  }
   117  
   118  // Stop a command thats running
   119  func (c *Command) Stop() error {
   120  	c.Lock()
   121  	defer c.Unlock()
   122  
   123  	if c.cmd != nil {
   124  		err := c.cmd.Process.Kill()
   125  		c.cmd = nil
   126  		return err
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  // Output of a running command
   133  func (c *Command) Output() ([]byte, error) {
   134  	c.Lock()
   135  	defer c.Unlock()
   136  	if c.cmd == nil {
   137  		return nil, errors.New("command is not running")
   138  	}
   139  	return c.cmdOutput.Bytes(), nil
   140  }
   141  
   142  // try is designed with command line executions in mind
   143  // Error should be checked and a simple `return` from the test case should
   144  // happen without calling `t.Fatal`. The error value should be disregarded.
   145  func Try(blockName string, t *T, f cmdFunc, maxTime time.Duration) error {
   146  	// hack. k8s can be slow locally
   147  	maxNano := float64(maxTime.Nanoseconds())
   148  	maxNano *= float64(maxTimeMultiplier)
   149  	// backoff, the retry logic is basically to cover up timing issues
   150  	maxNano += maxNano * float64(0.5) * float64(t.attempt-1)
   151  	start := time.Now()
   152  	var outp []byte
   153  	var err error
   154  
   155  	for {
   156  		if t.failed {
   157  			return ignoreThisError
   158  		}
   159  		if time.Since(start) > time.Duration(maxNano) {
   160  			_, file, line, _ := runtime.Caller(1)
   161  			fname := filepath.Base(file)
   162  			if err != nil {
   163  				t.Fatalf("%v:%v, %v (failed after %v with '%v'), output: '%v'", fname, line, blockName, time.Since(start), err, string(outp))
   164  				return ignoreThisError
   165  			}
   166  			return nil
   167  		}
   168  		outp, err = f()
   169  		if err == nil {
   170  			return nil
   171  		}
   172  		time.Sleep(1000 * time.Millisecond)
   173  	}
   174  }
   175  
   176  func once(blockName string, t *testing.T, f cmdFunc) {
   177  	outp, err := f()
   178  	if err != nil {
   179  		t.Fatalf("%v with '%v', output: %v", blockName, err, string(outp))
   180  	}
   181  }
   182  
   183  type ServerBase struct {
   184  	opts Options
   185  	cmd  *exec.Cmd
   186  	t    *T
   187  	// path to config file
   188  	config string
   189  	// directory for test config
   190  	dir string
   191  	// name of the environment
   192  	env string
   193  	// proxyPort number
   194  	proxyPort int
   195  	// apiPort number
   196  	apiPort int
   197  	// name of the container
   198  	container string
   199  	// namespace of server
   200  	namespace string
   201  }
   202  
   203  func getFrame(skipFrames int) runtime.Frame {
   204  	// We need the frame at index skipFrames+2, since we never want runtime.Callers and getFrame
   205  	targetFrameIndex := skipFrames + 2
   206  
   207  	// Set size to targetFrameIndex+2 to ensure we have room for one more caller than we need
   208  	programCounters := make([]uintptr, targetFrameIndex+2)
   209  	n := runtime.Callers(0, programCounters)
   210  
   211  	frame := runtime.Frame{Function: "unknown"}
   212  	if n > 0 {
   213  		frames := runtime.CallersFrames(programCounters[:n])
   214  		for more, frameIndex := true, 0; more && frameIndex <= targetFrameIndex; frameIndex++ {
   215  			var frameCandidate runtime.Frame
   216  			frameCandidate, more = frames.Next()
   217  			if frameIndex == targetFrameIndex {
   218  				frame = frameCandidate
   219  			}
   220  		}
   221  	}
   222  
   223  	return frame
   224  }
   225  
   226  // taken from https://stackoverflow.com/questions/35212985/is-it-possible-get-information-about-caller-function-in-golang
   227  // MyCaller returns the caller of the function that called it :)
   228  func myCaller() string {
   229  	// Skip GetCallerFunctionName and the function to get the caller of
   230  	return getFrame(2).Function
   231  }
   232  
   233  type Options struct {
   234  	// Login specifies whether to login to the server
   235  	Login bool
   236  	// Namespace to use, defaults to the test name
   237  	Namespace string
   238  	// Prevent generating default account
   239  	DisableAdmin bool
   240  }
   241  
   242  type Option func(o *Options)
   243  
   244  func WithLogin() Option {
   245  	return func(o *Options) {
   246  		o.Login = true
   247  	}
   248  }
   249  
   250  func WithDisableAdmin() Option {
   251  	return func(o *Options) {
   252  		o.DisableAdmin = true
   253  	}
   254  }
   255  
   256  func WithNamespace(ns string) Option {
   257  	return func(o *Options) {
   258  		o.Namespace = ns
   259  	}
   260  }
   261  
   262  func NewServer(t *T, opts ...Option) Server {
   263  	fname := strings.Split(myCaller(), ".")[2]
   264  	return newSrv(t, fname, opts...)
   265  }
   266  
   267  type NewServerFunc func(t *T, fname string, opts ...Option) Server
   268  
   269  var newSrv NewServerFunc = newLocalServer
   270  
   271  type ServerDefault struct {
   272  	ServerBase
   273  }
   274  
   275  func newLocalServer(t *T, fname string, opts ...Option) Server {
   276  	options := Options{
   277  		Namespace: fname,
   278  		Login:     false,
   279  	}
   280  	for _, o := range opts {
   281  		o(&options)
   282  	}
   283  
   284  	proxyPortnum := rand.Intn(maxPort-minPort) + minPort
   285  	apiPortNum := rand.Intn(maxPort-minPort) + minPort
   286  
   287  	// kill container, ignore error because it might not exist,
   288  	// we dont care about this that much
   289  	exec.Command("docker", "kill", fname).CombinedOutput()
   290  	exec.Command("docker", "rm", fname).CombinedOutput()
   291  
   292  	// run the server
   293  	cmd := exec.Command("docker", "run", "--name", fname,
   294  		fmt.Sprintf("-p=%v:8081", proxyPortnum),
   295  		fmt.Sprintf("-p=%v:8080", apiPortNum),
   296  		// "-e", "MICRO_PROFILE=ci",
   297  		"-e", fmt.Sprintf("MICRO_AUTH_DISABLE_ADMIN=%v", options.DisableAdmin),
   298  		"micro", "server")
   299  	configFile := configFile(fname)
   300  	return &ServerDefault{ServerBase{
   301  		dir:       filepath.Dir(configFile),
   302  		config:    configFile,
   303  		cmd:       cmd,
   304  		t:         t,
   305  		env:       options.Namespace,
   306  		container: fname,
   307  		apiPort:   apiPortNum,
   308  		proxyPort: proxyPortnum,
   309  		opts:      options,
   310  		namespace: "micro",
   311  	}}
   312  }
   313  
   314  func configFile(fname string) string {
   315  	dir := filepath.Join(user.Dir, "test")
   316  	return filepath.Join(dir, "config-"+fname+".json")
   317  }
   318  
   319  // error value should not be used but caller should return in the test suite
   320  // in case of error.
   321  func (s *ServerBase) Run() error {
   322  	go func() {
   323  		if err := s.cmd.Start(); err != nil {
   324  			s.t.Fatal(err)
   325  		}
   326  	}()
   327  
   328  	cmd := s.Command()
   329  
   330  	// add the environment
   331  	if err := Try("Adding micro env: "+s.env+" file: "+s.config, s.t, func() ([]byte, error) {
   332  		out, err := cmd.Exec("env", "add", s.env, fmt.Sprintf("127.0.0.1:%d", s.ProxyPort()))
   333  		if err != nil {
   334  			return out, err
   335  		}
   336  
   337  		if len(out) > 0 {
   338  			return out, errors.New("Unexpected output when adding env")
   339  		}
   340  
   341  		out, err = cmd.Exec("env")
   342  		if err != nil {
   343  			return out, err
   344  		}
   345  
   346  		if !strings.Contains(string(out), s.env) {
   347  			return out, errors.New("Can't find env added")
   348  		}
   349  
   350  		return out, nil
   351  	}, 15*time.Second); err != nil {
   352  		return err
   353  	}
   354  
   355  	return nil
   356  }
   357  
   358  func (s *ServerDefault) Run() error {
   359  	if err := s.ServerBase.Run(); err != nil {
   360  		return err
   361  	}
   362  
   363  	// login to admin account
   364  	if s.opts.Login {
   365  		Login(s, s.t, "admin", "micro")
   366  	}
   367  
   368  	servicesRequired := []string{"runtime", "registry", "broker", "config", "config", "proxy", "auth", "events", "store"}
   369  	if err := Try("Calling micro server", s.t, func() ([]byte, error) {
   370  		out, err := s.Command().Exec("services")
   371  		for _, s := range servicesRequired {
   372  			if !strings.Contains(string(out), s) {
   373  				return out, fmt.Errorf("Can't find %v: %v", s, err)
   374  			}
   375  		}
   376  
   377  		return out, err
   378  	}, 90*time.Second); err != nil {
   379  		return err
   380  	}
   381  
   382  	return nil
   383  }
   384  
   385  func (s *ServerBase) Close() {
   386  	// delete the config for this test
   387  	os.Remove(s.config)
   388  
   389  	// remove the credentials so they aren't reused on next run
   390  	s.Command().Exec("logout")
   391  
   392  	// reset back to the default namespace
   393  	namespace.Set("micro", s.env)
   394  
   395  }
   396  
   397  func (s *ServerDefault) Close() {
   398  	s.ServerBase.Close()
   399  	exec.Command("docker", "kill", s.container).CombinedOutput()
   400  	if s.cmd.Process != nil {
   401  		s.cmd.Process.Signal(syscall.SIGKILL)
   402  	}
   403  }
   404  
   405  func (s *ServerBase) Command() *Command {
   406  	return &Command{
   407  		Env:    s.env,
   408  		Config: s.config,
   409  		t:      s.t,
   410  	}
   411  }
   412  
   413  func (s *ServerBase) Env() string {
   414  	return s.env
   415  }
   416  
   417  func (s *ServerBase) ProxyPort() int {
   418  	return s.proxyPort
   419  }
   420  
   421  func (s *ServerBase) APIPort() int {
   422  	return s.apiPort
   423  }
   424  
   425  type T struct {
   426  	counter int
   427  	failed  bool
   428  	format  string
   429  	values  []interface{}
   430  	t       *testing.T
   431  	attempt int
   432  	waiting bool
   433  	started time.Time
   434  }
   435  
   436  // Failed indicate whether the test failed
   437  func (t *T) Failed() bool {
   438  	return t.failed
   439  }
   440  
   441  // Expose testing.T
   442  func (t *T) T() *testing.T {
   443  	return t.t
   444  }
   445  
   446  // Fatal logs and exits immediately. Assumes it has come from a TrySuite() call. If called from within goroutine it does not immediately exit.
   447  func (t *T) Fatal(values ...interface{}) {
   448  	t.t.Helper()
   449  	t.t.Log(values...)
   450  	t.failed = true
   451  	t.values = values
   452  	doPanic()
   453  }
   454  
   455  func (t *T) Log(values ...interface{}) {
   456  	t.t.Helper()
   457  	t.t.Log(values...)
   458  }
   459  
   460  func (t *T) Logf(format string, values ...interface{}) {
   461  	t.t.Helper()
   462  	t.t.Logf(format, values...)
   463  }
   464  
   465  // Fatalf logs and exits immediately. Assumes it has come from a TrySuite() call. If called from within goroutine it does not immediately exit.
   466  func (t *T) Fatalf(format string, values ...interface{}) {
   467  	t.t.Helper()
   468  	t.t.Log(fmt.Sprintf(format, values...))
   469  	t.failed = true
   470  	t.values = values
   471  	t.format = format
   472  	doPanic()
   473  }
   474  
   475  func doPanic() {
   476  	stack := debug.Stack()
   477  	// if we're not in TrySuite we're doing something funky in a goroutine (probably), don't panic because we won't recover
   478  	if !strings.Contains(string(stack), "TrySuite(") {
   479  		return
   480  	}
   481  	panic(errFatal)
   482  }
   483  
   484  func (t *T) Parallel() {
   485  	if t.counter == 0 && isParallel {
   486  		t.waiting = true
   487  		t.t.Parallel()
   488  		t.started = time.Now()
   489  		t.waiting = false
   490  	}
   491  	t.counter++
   492  }
   493  
   494  // New returns a new test framework
   495  func New(t *testing.T) *T {
   496  	return &T{t: t, attempt: 1}
   497  }
   498  
   499  // TrySuite is designed to retry a TestXX function
   500  func TrySuite(t *testing.T, f func(t *T), times int) {
   501  	t.Helper()
   502  	caller := strings.Split(getFrame(1).Function, ".")[2]
   503  	if len(testFilter) > 0 {
   504  		runit := false
   505  		for _, test := range testFilter {
   506  			if test == caller {
   507  				runit = true
   508  				break
   509  			}
   510  		}
   511  		if !runit {
   512  			t.Skip()
   513  		}
   514  	}
   515  	timeout := os.Getenv("MICRO_TEST_TIMEOUT")
   516  	td, err := time.ParseDuration(timeout)
   517  	if err != nil {
   518  		td = 3 * time.Minute
   519  	}
   520  	timeoutCh := time.After(td)
   521  	done := make(chan bool)
   522  	start := time.Now()
   523  	tee := New(t)
   524  	go func() {
   525  		for i := 0; i < times; i++ {
   526  			wrapF(tee, f)
   527  			if !tee.failed {
   528  				done <- true
   529  				return
   530  			}
   531  			if i != times-1 {
   532  				tee.failed = false
   533  			}
   534  			tee.attempt++
   535  			time.Sleep(200 * time.Millisecond)
   536  		}
   537  		done <- true
   538  	}()
   539  	for {
   540  		select {
   541  		case <-timeoutCh:
   542  			if tee.waiting {
   543  				// not started yet, let's check back later
   544  				timeoutCh = time.After(td)
   545  				continue
   546  			}
   547  			if !tee.started.IsZero() && time.Since(tee.started) < td {
   548  				// not timed out since the actual start time, reset
   549  				timeoutCh = time.After(td - time.Since(tee.started))
   550  				continue
   551  			}
   552  			_, file, line, _ := runtime.Caller(1)
   553  			fname := filepath.Base(file)
   554  			actualStart := start
   555  			if !tee.started.IsZero() {
   556  				actualStart = tee.started
   557  			}
   558  			t.Fatalf("%v:%v, %v (failed after %v)", fname, line, caller, time.Since(actualStart))
   559  			return
   560  		case <-done:
   561  			if tee.failed {
   562  				if t.Failed() {
   563  					return
   564  				}
   565  				if len(tee.format) > 0 {
   566  					t.Fatalf(tee.format, tee.values...)
   567  				} else {
   568  					t.Fatal(tee.values...)
   569  				}
   570  			}
   571  			return
   572  		}
   573  	}
   574  }
   575  
   576  func wrapF(t *T, f func(t *T)) {
   577  	defer func() {
   578  		if r := recover(); r != nil {
   579  			if r != errFatal {
   580  				panic(r)
   581  			}
   582  		}
   583  	}()
   584  	f(t)
   585  }
   586  
   587  func Login(serv Server, t *T, email, password string) error {
   588  	return Try("Logging in with "+email, t, func() ([]byte, error) {
   589  		out, err := serv.Command().Exec("login", "--email", email, "--password", password)
   590  		if err != nil {
   591  			return out, err
   592  		}
   593  		if !strings.Contains(string(out), "Success") {
   594  			return out, errors.New("Login output does not contain 'Success'")
   595  		}
   596  		return out, err
   597  	}, 15*time.Second)
   598  }
   599  
   600  func ChangeNamespace(cmd *Command, env, namespace string) error {
   601  	outp, err := cmd.Exec("user", "config", "get", "namespaces."+env+".all")
   602  	if err != nil {
   603  		return err
   604  	}
   605  	parts := strings.Split(string(outp), ".")
   606  	index := map[string]struct{}{}
   607  	for _, part := range parts {
   608  		if len(strings.TrimSpace(part)) == 0 {
   609  			continue
   610  		}
   611  		index[part] = struct{}{}
   612  	}
   613  	index[namespace] = struct{}{}
   614  	list := []string{}
   615  	for k, _ := range index {
   616  		list = append(list, k)
   617  	}
   618  	if _, err := cmd.Exec("user", "config", "set", "namespaces."+env+".all", strings.Join(list, ",")); err != nil {
   619  		return err
   620  	}
   621  	if _, err := cmd.Exec("user", "config", "set", "namespaces."+env+".current", namespace); err != nil {
   622  		return err
   623  	}
   624  	return nil
   625  }