github.com/bytebase/air@v0.0.0-20221010164341-87187cc8b920/runner/engine_test.go (about) 1 package runner 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "net" 8 "os" 9 "os/exec" 10 "os/signal" 11 "runtime" 12 "strings" 13 "syscall" 14 "testing" 15 "time" 16 17 "github.com/pelletier/go-toml" 18 "github.com/stretchr/testify/assert" 19 ) 20 21 func TestNewEngine(t *testing.T) { 22 _ = os.Unsetenv(airWd) 23 engine, err := NewEngine("", true) 24 if err != nil { 25 t.Fatalf("Should not be fail: %s.", err) 26 } 27 if engine.logger == nil { 28 t.Fatal("logger should not be nil") 29 } 30 if engine.config == nil { 31 t.Fatal("Config should not be nil") 32 } 33 if engine.watcher == nil { 34 t.Fatal("watcher should not be nil") 35 } 36 } 37 38 func TestCheckRunEnv(t *testing.T) { 39 _ = os.Unsetenv(airWd) 40 engine, err := NewEngine("", true) 41 if err != nil { 42 t.Fatalf("Should not be fail: %s.", err) 43 } 44 err = engine.checkRunEnv() 45 if err == nil { 46 t.Fatal("should throw a err") 47 } 48 } 49 50 func TestWatching(t *testing.T) { 51 engine, err := NewEngine("", true) 52 if err != nil { 53 t.Fatalf("Should not be fail: %s.", err) 54 } 55 path, err := os.Getwd() 56 if err != nil { 57 t.Fatalf("Should not be fail: %s.", err) 58 } 59 path = strings.Replace(path, "_testdata/toml", "", 1) 60 err = engine.watching(path + "/_testdata/watching") 61 if err != nil { 62 t.Fatalf("Should not be fail: %s.", err) 63 } 64 } 65 66 func TestRegexes(t *testing.T) { 67 engine, err := NewEngine("", true) 68 if err != nil { 69 t.Fatalf("Should not be fail: %s.", err) 70 } 71 engine.config.Build.ExcludeRegex = []string{"foo\\.html$", "bar", "_test\\.go"} 72 73 result, err := engine.isExcludeRegex("./test/foo.html") 74 if err != nil { 75 t.Fatalf("Should not be fail: %s.", err) 76 } 77 if result != true { 78 t.Errorf("expected '%t' but got '%t'", true, result) 79 } 80 81 result, err = engine.isExcludeRegex("./test/bar/index.html") 82 if err != nil { 83 t.Fatalf("Should not be fail: %s.", err) 84 } 85 if result != true { 86 t.Errorf("expected '%t' but got '%t'", true, result) 87 } 88 89 result, err = engine.isExcludeRegex("./test/unrelated.html") 90 if err != nil { 91 t.Fatalf("Should not be fail: %s.", err) 92 } 93 if result { 94 t.Errorf("expected '%t' but got '%t'", false, result) 95 } 96 97 result, err = engine.isExcludeRegex("./myPackage/goFile_testxgo") 98 if err != nil { 99 t.Fatalf("Should not be fail: %s.", err) 100 } 101 if result { 102 t.Errorf("expected '%t' but got '%t'", false, result) 103 } 104 result, err = engine.isExcludeRegex("./myPackage/goFile_test.go") 105 if err != nil { 106 t.Fatalf("Should not be fail: %s.", err) 107 } 108 if result != true { 109 t.Errorf("expected '%t' but got '%t'", true, result) 110 } 111 } 112 113 func TestRunBin(t *testing.T) { 114 engine, err := NewEngine("", true) 115 if err != nil { 116 t.Fatalf("Should not be fail: %s.", err) 117 } 118 119 err = engine.runBin() 120 if err != nil { 121 t.Fatalf("Should not be fail: %s.", err) 122 } 123 } 124 125 func GetPort() (int, func()) { 126 l, err := net.Listen("tcp", ":0") 127 port := l.Addr().(*net.TCPAddr).Port 128 if err != nil { 129 panic(err) 130 } 131 return port, func() { 132 _ = l.Close() 133 } 134 } 135 136 func TestRebuild(t *testing.T) { 137 // generate a random port 138 port, f := GetPort() 139 f() 140 t.Logf("port: %d", port) 141 142 tmpDir := initTestEnv(t, port) 143 // change dir to tmpDir 144 err := os.Chdir(tmpDir) 145 if err != nil { 146 t.Fatalf("Should not be fail: %s.", err) 147 } 148 engine, err := NewEngine("", true) 149 engine.config.Build.ExcludeUnchanged = true 150 if err != nil { 151 t.Fatalf("Should not be fail: %s.", err) 152 } 153 go func() { 154 engine.Run() 155 t.Logf("engine stopped") 156 }() 157 err = waitingPortReady(t, port, time.Second*10) 158 if err != nil { 159 t.Fatalf("Should not be fail: %s.", err) 160 } 161 t.Logf("port is ready") 162 163 // start rebuld 164 165 t.Logf("start change main.go") 166 // change file of main.go 167 // just append a new empty line to main.go 168 time.Sleep(time.Second * 2) 169 file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0644) 170 if err != nil { 171 t.Fatalf("Should not be fail: %s.", err) 172 } 173 defer file.Close() 174 _, err = file.WriteString("\n") 175 if err != nil { 176 t.Fatalf("Should not be fail: %s.", err) 177 } 178 err = waitingPortConnectionRefused(t, port, time.Second*10) 179 if err != nil { 180 t.Fatalf("timeout: %s.", err) 181 } 182 t.Logf("connection refused") 183 time.Sleep(time.Second * 2) 184 err = waitingPortReady(t, port, time.Second*10) 185 if err != nil { 186 t.Fatalf("Should not be fail: %s.", err) 187 } 188 t.Logf("port is ready") 189 // stop engine 190 engine.Stop() 191 time.Sleep(time.Second * 1) 192 t.Logf("engine stopped") 193 assert.True(t, checkPortConnectionRefused(port)) 194 } 195 196 func waitingPortConnectionRefused(t *testing.T, port int, timeout time.Duration) error { 197 t.Logf("waiting port %d connection refused", port) 198 timer := time.NewTimer(timeout) 199 ticker := time.NewTicker(time.Millisecond * 100) 200 defer ticker.Stop() 201 defer timer.Stop() 202 for { 203 select { 204 case <-timer.C: 205 return fmt.Errorf("timeout") 206 case <-ticker.C: 207 print(".") 208 _, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) 209 if errors.Is(err, syscall.ECONNREFUSED) { 210 return nil 211 } 212 time.Sleep(time.Millisecond * 100) 213 } 214 } 215 } 216 217 func TestCtrlCWhenHaveKillDelay(t *testing.T) { 218 // fix https://github.com/cosmtrek/air/issues/278 219 // generate a random port 220 data := []byte("[build]\n kill_delay = \"2s\"") 221 c := Config{} 222 if err := toml.Unmarshal(data, &c); err != nil { 223 t.Fatalf("Should not be fail: %s.", err) 224 } 225 226 port, f := GetPort() 227 f() 228 t.Logf("port: %d", port) 229 230 tmpDir := initTestEnv(t, port) 231 // change dir to tmpDir 232 err := os.Chdir(tmpDir) 233 if err != nil { 234 t.Fatalf("Should not be fail: %s.", err) 235 } 236 engine, err := NewEngine("", true) 237 if err != nil { 238 t.Fatalf("Should not be fail: %s.", err) 239 } 240 engine.config.Build.KillDelay = c.Build.KillDelay 241 engine.config.Build.Delay = 2000 242 engine.config.Build.SendInterrupt = true 243 engine.config.preprocess() 244 245 go func() { 246 engine.Run() 247 t.Logf("engine stopped") 248 }() 249 sigs := make(chan os.Signal, 1) 250 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 251 go func() { 252 <-sigs 253 engine.Stop() 254 t.Logf("engine stopped") 255 }() 256 if err := waitingPortReady(t, port, time.Second*10); err != nil { 257 t.Fatalf("Should not be fail: %s.", err) 258 } 259 sigs <- syscall.SIGINT 260 err = waitingPortConnectionRefused(t, port, time.Second*10) 261 if err != nil { 262 t.Fatalf("Should not be fail: %s.", err) 263 } 264 time.Sleep(time.Second * 3) 265 } 266 267 func TestCtrlCWhenREngineIsRunning(t *testing.T) { 268 // generate a random port 269 port, f := GetPort() 270 f() 271 t.Logf("port: %d", port) 272 273 tmpDir := initTestEnv(t, port) 274 // change dir to tmpDir 275 err := os.Chdir(tmpDir) 276 if err != nil { 277 t.Fatalf("Should not be fail: %s.", err) 278 } 279 engine, err := NewEngine("", true) 280 if err != nil { 281 t.Fatalf("Should not be fail: %s.", err) 282 } 283 go func() { 284 engine.Run() 285 t.Logf("engine stopped") 286 }() 287 sigs := make(chan os.Signal, 1) 288 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 289 go func() { 290 <-sigs 291 engine.Stop() 292 t.Logf("engine stopped") 293 }() 294 if err := waitingPortReady(t, port, time.Second*10); err != nil { 295 t.Fatalf("Should not be fail: %s.", err) 296 } 297 sigs <- syscall.SIGINT 298 time.Sleep(time.Second * 1) 299 err = waitingPortConnectionRefused(t, port, time.Second*10) 300 if err != nil { 301 t.Fatalf("Should not be fail: %s.", err) 302 } 303 } 304 305 func TestFixCloseOfChannelAfterCtrlC(t *testing.T) { 306 // fix https://github.com/cosmtrek/air/issues/294 307 dir := initWithBuildFailedCode(t) 308 309 err := os.Chdir(dir) 310 if err != nil { 311 t.Fatalf("Should not be fail: %s.", err) 312 } 313 engine, err := NewEngine("", true) 314 if err != nil { 315 t.Fatalf("Should not be fail: %s.", err) 316 } 317 sigs := make(chan os.Signal, 1) 318 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 319 go func() { 320 engine.Run() 321 t.Logf("engine stopped") 322 }() 323 324 go func() { 325 <-sigs 326 engine.Stop() 327 t.Logf("engine stopped") 328 }() 329 // waiting for compile error 330 time.Sleep(time.Second * 3) 331 port, f := GetPort() 332 f() 333 // correct code 334 err = generateGoCode(dir, port) 335 if err != nil { 336 t.Fatalf("Should not be fail: %s.", err) 337 } 338 339 if err := waitingPortReady(t, port, time.Second*10); err != nil { 340 t.Fatalf("Should not be fail: %s.", err) 341 } 342 343 // ctrl + c 344 sigs <- syscall.SIGINT 345 time.Sleep(time.Second * 1) 346 if err := waitingPortConnectionRefused(t, port, time.Second*10); err != nil { 347 t.Fatalf("Should not be fail: %s.", err) 348 } 349 } 350 351 func TestFixCloseOfChannelAfterTwoFailedBuild(t *testing.T) { 352 // fix https://github.com/cosmtrek/air/issues/294 353 // happens after two failed builds 354 dir := initWithBuildFailedCode(t) 355 // change dir to tmpDir 356 err := os.Chdir(dir) 357 if err != nil { 358 t.Fatalf("Should not be fail: %s.", err) 359 } 360 engine, err := NewEngine("", true) 361 if err != nil { 362 t.Fatalf("Should not be fail: %s.", err) 363 } 364 sigs := make(chan os.Signal, 1) 365 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 366 go func() { 367 engine.Run() 368 t.Logf("engine stopped") 369 }() 370 371 go func() { 372 <-sigs 373 engine.Stop() 374 t.Logf("engine stopped") 375 }() 376 377 // waiting for compile error 378 time.Sleep(time.Second * 3) 379 380 // edit *.go file to create build error again 381 file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0644) 382 if err != nil { 383 t.Fatalf("Should not be fail: %s.", err) 384 } 385 defer file.Close() 386 _, err = file.WriteString("\n") 387 if err != nil { 388 t.Fatalf("Should not be fail: %s.", err) 389 } 390 time.Sleep(time.Second * 3) 391 // ctrl + c 392 sigs <- syscall.SIGINT 393 time.Sleep(time.Second * 1) 394 } 395 396 // waitingPortReady waits until the port is ready to be used. 397 func waitingPortReady(t *testing.T, port int, timeout time.Duration) error { 398 t.Logf("waiting port %d ready", port) 399 timeoutChan := time.After(timeout) 400 ticker := time.NewTicker(time.Millisecond * 100) 401 defer ticker.Stop() 402 for { 403 select { 404 case <-timeoutChan: 405 return fmt.Errorf("timeout") 406 case <-ticker.C: 407 conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) 408 if err == nil { 409 _ = conn.Close() 410 return nil 411 } 412 } 413 } 414 } 415 416 func TestRun(t *testing.T) { 417 // generate a random port 418 port, f := GetPort() 419 f() 420 t.Logf("port: %d", port) 421 422 tmpDir := initTestEnv(t, port) 423 // change dir to tmpDir 424 err := os.Chdir(tmpDir) 425 if err != nil { 426 t.Fatalf("Should not be fail: %s.", err) 427 } 428 engine, err := NewEngine("", true) 429 if err != nil { 430 t.Fatalf("Should not be fail: %s.", err) 431 } 432 433 go func() { 434 engine.Run() 435 }() 436 time.Sleep(time.Second * 2) 437 assert.True(t, checkPortHaveBeenUsed(port)) 438 t.Logf("try to stop") 439 engine.Stop() 440 time.Sleep(time.Second * 1) 441 assert.False(t, checkPortHaveBeenUsed(port)) 442 t.Logf("stoped") 443 } 444 445 func checkPortConnectionRefused(port int) bool { 446 conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) 447 defer func() { 448 if conn != nil { 449 _ = conn.Close() 450 } 451 }() 452 if errors.Is(err, syscall.ECONNREFUSED) { 453 return true 454 } 455 return false 456 } 457 458 func checkPortHaveBeenUsed(port int) bool { 459 conn, err := net.Dial("tcp", fmt.Sprintf(":%d", port)) 460 if err != nil { 461 return false 462 } 463 _ = conn.Close() 464 return true 465 } 466 467 func initTestEnv(t *testing.T, port int) string { 468 tempDir := t.TempDir() 469 t.Logf("tempDir: %s", tempDir) 470 // generate golang code to tempdir 471 err := generateGoCode(tempDir, port) 472 if err != nil { 473 t.Fatalf("Should not be fail: %s.", err) 474 } 475 return tempDir 476 } 477 478 func initWithBuildFailedCode(t *testing.T) string { 479 tempDir := t.TempDir() 480 t.Logf("tempDir: %s", tempDir) 481 // generate golang code to tempdir 482 err := generateBuildErrorGoCode(tempDir) 483 if err != nil { 484 t.Fatalf("Should not be fail: %s.", err) 485 } 486 return tempDir 487 } 488 489 func generateBuildErrorGoCode(dir string) error { 490 code := `package main 491 // You can edit this code! 492 // Click here and start typing. 493 494 func main() { 495 Println("Hello, 世界") 496 497 } 498 ` 499 file, err := os.Create(dir + "/main.go") 500 if err != nil { 501 return err 502 } 503 _, err = file.WriteString(code) 504 505 // generate go mod file 506 mod := `module air.sample.com 507 508 go 1.17 509 ` 510 file, err = os.Create(dir + "/go.mod") 511 if err != nil { 512 return err 513 } 514 _, err = file.WriteString(mod) 515 if err != nil { 516 return err 517 } 518 return nil 519 } 520 521 // generateGoCode generates golang code to tempdir 522 func generateGoCode(dir string, port int) error { 523 524 code := fmt.Sprintf(`package main 525 526 import ( 527 "log" 528 "net/http" 529 ) 530 531 func main() { 532 log.Fatal(http.ListenAndServe(":%v", nil)) 533 } 534 `, port) 535 file, err := os.Create(dir + "/main.go") 536 if err != nil { 537 return err 538 } 539 _, err = file.WriteString(code) 540 541 // generate go mod file 542 mod := `module air.sample.com 543 544 go 1.17 545 ` 546 file, err = os.Create(dir + "/go.mod") 547 if err != nil { 548 return err 549 } 550 _, err = file.WriteString(mod) 551 if err != nil { 552 return err 553 } 554 return nil 555 } 556 557 func TestRebuildWhenRunCmdUsingDLV(t *testing.T) { 558 // generate a random port 559 port, f := GetPort() 560 f() 561 t.Logf("port: %d", port) 562 tmpDir := initTestEnv(t, port) 563 // change dir to tmpDir 564 err := os.Chdir(tmpDir) 565 if err != nil { 566 t.Fatalf("Should not be fail: %s.", err) 567 } 568 engine, err := NewEngine("", true) 569 if err != nil { 570 t.Fatalf("Should not be fail: %s.", err) 571 } 572 engine.config.Build.Cmd = "go build -gcflags='all=-N -l' -o ./tmp/main ." 573 engine.config.Build.Bin = "" 574 engine.config.Build.FullBin = "dlv exec --accept-multiclient --log --headless --continue --listen :2345 --api-version 2 ./tmp/main" 575 _ = engine.config.preprocess() 576 go func() { 577 engine.Run() 578 }() 579 if err := waitingPortReady(t, port, time.Second*40); err != nil { 580 t.Fatalf("Should not be fail: %s.", err) 581 } 582 583 t.Logf("start change main.go") 584 // change file of main.go 585 // just append a new empty line to main.go 586 time.Sleep(time.Second * 2) 587 go func() { 588 file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0644) 589 if err != nil { 590 t.Fatalf("Should not be fail: %s.", err) 591 } 592 defer file.Close() 593 _, err = file.WriteString("\n") 594 if err != nil { 595 t.Fatalf("Should not be fail: %s.", err) 596 } 597 }() 598 err = waitingPortConnectionRefused(t, port, time.Second*10) 599 if err != nil { 600 t.Fatalf("timeout: %s.", err) 601 } 602 t.Logf("connection refused") 603 time.Sleep(time.Second * 2) 604 err = waitingPortReady(t, port, time.Second*40) 605 if err != nil { 606 t.Fatalf("Should not be fail: %s.", err) 607 } 608 t.Logf("port is ready") 609 // stop engine 610 engine.Stop() 611 time.Sleep(time.Second * 3) 612 t.Logf("engine stopped") 613 assert.True(t, checkPortConnectionRefused(port)) 614 } 615 616 func TestWriteDefaultConfig(t *testing.T) { 617 port, f := GetPort() 618 f() 619 t.Logf("port: %d", port) 620 621 tmpDir := initTestEnv(t, port) 622 // change dir to tmpDir 623 if err := os.Chdir(tmpDir); err != nil { 624 t.Fatal(err) 625 } 626 writeDefaultConfig() 627 // check the file is exist 628 if _, err := os.Stat(dftTOML); err != nil { 629 t.Fatal(err) 630 } 631 632 // check the file content is right 633 actual, err := readConfig(dftTOML) 634 if err != nil { 635 t.Fatal(err) 636 } 637 expect := defaultConfig() 638 639 assert.Equal(t, expect, *actual) 640 } 641 642 func TestCheckNilSliceShouldBeenOverwrite(t *testing.T) { 643 port, f := GetPort() 644 f() 645 t.Logf("port: %d", port) 646 647 tmpDir := initTestEnv(t, port) 648 649 // change dir to tmpDir 650 if err := os.Chdir(tmpDir); err != nil { 651 t.Fatal(err) 652 } 653 654 // write easy config file 655 656 config := ` 657 [build] 658 cmd = "go build ." 659 bin = "tmp/main" 660 exclude_regex = [] 661 exclude_dir = ["test"] 662 exclude_file = ["main.go"] 663 664 ` 665 if err := ioutil.WriteFile(dftTOML, []byte(config), 0644); err != nil { 666 t.Fatal(err) 667 } 668 engine, err := NewEngine(".air.toml", true) 669 if err != nil { 670 t.Fatal(err) 671 } 672 assert.Equal(t, []string{"go", "tpl", "tmpl", "html"}, engine.config.Build.IncludeExt) 673 assert.Equal(t, []string{}, engine.config.Build.ExcludeRegex) 674 assert.Equal(t, []string{"test"}, engine.config.Build.ExcludeDir) 675 // add new config 676 assert.Equal(t, []string{"main.go"}, engine.config.Build.ExcludeFile) 677 assert.Equal(t, "go build .", engine.config.Build.Cmd) 678 679 } 680 681 func TestShouldIncludeGoTestFile(t *testing.T) { 682 port, f := GetPort() 683 f() 684 t.Logf("port: %d", port) 685 686 tmpDir := initTestEnv(t, port) 687 // change dir to tmpDir 688 if err := os.Chdir(tmpDir); err != nil { 689 t.Fatal(err) 690 } 691 writeDefaultConfig() 692 693 // write go test file 694 file, err := os.Create("main_test.go") 695 if err != nil { 696 t.Fatal(err) 697 } 698 _, err = file.WriteString(`package main 699 700 import "testing" 701 702 func Test(t *testing.T) { 703 t.Log("testing") 704 } 705 `) 706 // run sed 707 // check the file is exist 708 if _, err := os.Stat(dftTOML); err != nil { 709 t.Fatal(err) 710 } 711 // check is MacOS 712 var cmd *exec.Cmd 713 if runtime.GOOS == "darwin" { 714 cmd = exec.Command("gsed", "-i", "s/\"_test.*go\"//g", ".air.toml") 715 } else { 716 cmd = exec.Command("sed", "-i", "s/\"_test.*go\"//g", ".air.toml") 717 } 718 cmd.Stdout = os.Stdout 719 cmd.Stderr = os.Stderr 720 if err := cmd.Run(); err != nil { 721 t.Fatal(err) 722 } 723 724 time.Sleep(time.Second * 3) 725 engine, err := NewEngine(".air.toml", false) 726 if err != nil { 727 t.Fatal(err) 728 } 729 go func() { 730 engine.Run() 731 }() 732 733 t.Logf("start change main_test.go") 734 // change file of main_test.go 735 // just append a new empty line to main_test.go 736 if err = waitingPortReady(t, port, time.Second*40); err != nil { 737 t.Fatal(err) 738 } 739 go func() { 740 file, err := os.OpenFile("main_test.go", os.O_APPEND|os.O_WRONLY, 0644) 741 if err != nil { 742 t.Fatalf("Should not be fail: %s.", err) 743 } 744 defer file.Close() 745 _, err = file.WriteString("\n") 746 if err != nil { 747 t.Fatalf("Should not be fail: %s.", err) 748 } 749 }() 750 // should Have rebuild 751 if err = waitingPortConnectionRefused(t, port, time.Second*10); err != nil { 752 t.Fatal(err) 753 } 754 } 755 756 func TestCreateNewDir(t *testing.T) { 757 // generate a random port 758 port, f := GetPort() 759 f() 760 t.Logf("port: %d", port) 761 762 tmpDir := initTestEnv(t, port) 763 // change dir to tmpDir 764 err := os.Chdir(tmpDir) 765 if err != nil { 766 t.Fatalf("Should not be fail: %s.", err) 767 } 768 engine, err := NewEngine("", true) 769 if err != nil { 770 t.Fatalf("Should not be fail: %s.", err) 771 } 772 773 go func() { 774 engine.Run() 775 }() 776 time.Sleep(time.Second * 2) 777 assert.True(t, checkPortHaveBeenUsed(port)) 778 779 // create a new dir make dir 780 if err = os.Mkdir(tmpDir+"/dir", 0644); err != nil { 781 t.Fatal(err) 782 } 783 784 // no need reload 785 if err = waitingPortConnectionRefused(t, port, 3*time.Second); err == nil { 786 t.Fatal("should raise a error") 787 } 788 engine.Stop() 789 time.Sleep(2 * time.Second) 790 791 }