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 }