github.com/zhb127/air@v0.0.2-0.20231109030911-fb911e430cdd/runner/engine_test.go (about) 1 package runner 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "net" 8 "os" 9 "os/exec" 10 "os/signal" 11 "runtime" 12 "strings" 13 "sync" 14 "sync/atomic" 15 "syscall" 16 "testing" 17 "time" 18 19 "github.com/pelletier/go-toml" 20 "github.com/stretchr/testify/assert" 21 ) 22 23 func TestNewEngine(t *testing.T) { 24 _ = os.Unsetenv(airWd) 25 engine, err := NewEngine("", true) 26 if err != nil { 27 t.Fatalf("Should not be fail: %s.", err) 28 } 29 if engine.logger == nil { 30 t.Fatal("logger should not be nil") 31 } 32 if engine.config == nil { 33 t.Fatal("Config should not be nil") 34 } 35 if engine.watcher == nil { 36 t.Fatal("watcher should not be nil") 37 } 38 } 39 40 func TestCheckRunEnv(t *testing.T) { 41 _ = os.Unsetenv(airWd) 42 engine, err := NewEngine("", true) 43 if err != nil { 44 t.Fatalf("Should not be fail: %s.", err) 45 } 46 err = engine.checkRunEnv() 47 if err == nil { 48 t.Fatal("should throw a err") 49 } 50 } 51 52 func TestWatching(t *testing.T) { 53 engine, err := NewEngine("", true) 54 if err != nil { 55 t.Fatalf("Should not be fail: %s.", err) 56 } 57 path, err := os.Getwd() 58 if err != nil { 59 t.Fatalf("Should not be fail: %s.", err) 60 } 61 path = strings.Replace(path, "_testdata/toml", "", 1) 62 err = engine.watching(path + "/_testdata/watching") 63 if err != nil { 64 t.Fatalf("Should not be fail: %s.", err) 65 } 66 } 67 68 func TestRegexes(t *testing.T) { 69 engine, err := NewEngine("", true) 70 if err != nil { 71 t.Fatalf("Should not be fail: %s.", err) 72 } 73 engine.config.Build.ExcludeRegex = []string{"foo\\.html$", "bar", "_test\\.go"} 74 75 result, err := engine.isExcludeRegex("./test/foo.html") 76 if err != nil { 77 t.Fatalf("Should not be fail: %s.", err) 78 } 79 if result != true { 80 t.Errorf("expected '%t' but got '%t'", true, result) 81 } 82 83 result, err = engine.isExcludeRegex("./test/bar/index.html") 84 if err != nil { 85 t.Fatalf("Should not be fail: %s.", err) 86 } 87 if result != true { 88 t.Errorf("expected '%t' but got '%t'", true, result) 89 } 90 91 result, err = engine.isExcludeRegex("./test/unrelated.html") 92 if err != nil { 93 t.Fatalf("Should not be fail: %s.", err) 94 } 95 if result { 96 t.Errorf("expected '%t' but got '%t'", false, result) 97 } 98 99 result, err = engine.isExcludeRegex("./myPackage/goFile_testxgo") 100 if err != nil { 101 t.Fatalf("Should not be fail: %s.", err) 102 } 103 if result { 104 t.Errorf("expected '%t' but got '%t'", false, result) 105 } 106 result, err = engine.isExcludeRegex("./myPackage/goFile_test.go") 107 if err != nil { 108 t.Fatalf("Should not be fail: %s.", err) 109 } 110 if result != true { 111 t.Errorf("expected '%t' but got '%t'", true, result) 112 } 113 } 114 115 func TestRerun(t *testing.T) { 116 tmpDir := initWithQuickExitGoCode(t) 117 // change dir to tmpDir 118 chdir(t, tmpDir) 119 engine, err := NewEngine("", true) 120 engine.config.Build.ExcludeUnchanged = true 121 engine.config.Build.Rerun = true 122 engine.config.Build.RerunDelay = 100 123 if err != nil { 124 t.Fatalf("Should not be fail: %s.", err) 125 } 126 go func() { 127 engine.Run() 128 t.Logf("engine run") 129 }() 130 131 time.Sleep(time.Second * 1) 132 133 // stop engine 134 engine.Stop() 135 time.Sleep(time.Second * 1) 136 t.Logf("engine stopped") 137 138 if atomic.LoadUint64(&engine.round) <= 1 { 139 t.Fatalf("The engine did't rerun") 140 } 141 } 142 143 func TestRerunWhenFileChanged(t *testing.T) { 144 tmpDir := initWithQuickExitGoCode(t) 145 // change dir to tmpDir 146 chdir(t, tmpDir) 147 engine, err := NewEngine("", true) 148 engine.config.Build.ExcludeUnchanged = true 149 engine.config.Build.Rerun = true 150 engine.config.Build.RerunDelay = 100 151 if err != nil { 152 t.Fatalf("Should not be fail: %s.", err) 153 } 154 go func() { 155 engine.Run() 156 t.Logf("engine run") 157 }() 158 time.Sleep(time.Second * 1) 159 160 roundBeforeChange := atomic.LoadUint64(&engine.round) 161 162 t.Logf("start change main.go") 163 // change file of main.go 164 // just append a new empty line to main.go 165 time.Sleep(time.Second * 2) 166 file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0o644) 167 if err != nil { 168 t.Fatalf("Should not be fail: %s.", err) 169 } 170 defer file.Close() 171 _, err = file.WriteString("\n") 172 if err != nil { 173 t.Fatalf("Should not be fail: %s.", err) 174 } 175 176 time.Sleep(time.Second * 1) 177 // stop engine 178 engine.Stop() 179 time.Sleep(time.Second * 1) 180 t.Logf("engine stopped") 181 182 roundAfterChange := atomic.LoadUint64(&engine.round) 183 if roundBeforeChange+1 >= roundAfterChange { 184 t.Fatalf("The engine didn't rerun") 185 } 186 } 187 188 func TestRunCommand(t *testing.T) { 189 // generate a random port 190 port, f := GetPort() 191 f() 192 t.Logf("port: %d", port) 193 tmpDir := initTestEnv(t, port) 194 // change dir to tmpDir 195 chdir(t, tmpDir) 196 engine, err := NewEngine("", true) 197 if err != nil { 198 t.Fatalf("Should not be fail: %s.", err) 199 } 200 err = engine.runCommand("touch test.txt") 201 if err != nil { 202 t.Fatalf("Should not be fail: %s.", err) 203 } 204 if _, err := os.Stat("./test.txt"); err != nil { 205 if os.IsNotExist(err) { 206 t.Fatalf("Should not be fail: %s.", err) 207 } 208 } 209 } 210 211 func TestRunPreCmd(t *testing.T) { 212 // generate a random port 213 port, f := GetPort() 214 f() 215 t.Logf("port: %d", port) 216 tmpDir := initTestEnv(t, port) 217 // change dir to tmpDir 218 chdir(t, tmpDir) 219 engine, err := NewEngine("", true) 220 if err != nil { 221 t.Fatalf("Should not be fail: %s.", err) 222 } 223 engine.config.Build.PreCmd = []string{"echo 'hello air' > pre_cmd.txt"} 224 err = engine.runPreCmd() 225 if err != nil { 226 t.Fatalf("Should not be fail: %s.", err) 227 } 228 if _, err := os.Stat("./pre_cmd.txt"); err != nil { 229 if os.IsNotExist(err) { 230 t.Fatalf("Should not be fail: %s.", err) 231 } 232 } 233 } 234 235 func TestRunPostCmd(t *testing.T) { 236 // generate a random port 237 port, f := GetPort() 238 f() 239 t.Logf("port: %d", port) 240 tmpDir := initTestEnv(t, port) 241 // change dir to tmpDir 242 chdir(t, tmpDir) 243 244 engine, err := NewEngine("", true) 245 if err != nil { 246 t.Fatalf("Should not be fail: %s.", err) 247 } 248 249 engine.config.Build.PostCmd = []string{"echo 'hello air' > post_cmd.txt"} 250 err = engine.runPostCmd() 251 if err != nil { 252 t.Fatalf("Should not be fail: %s.", err) 253 } 254 255 if _, err := os.Stat("./post_cmd.txt"); err != nil { 256 if os.IsNotExist(err) { 257 t.Fatalf("Should not be fail: %s.", err) 258 } 259 } 260 } 261 262 func TestRunBin(t *testing.T) { 263 engine, err := NewEngine("", true) 264 if err != nil { 265 t.Fatalf("Should not be fail: %s.", err) 266 } 267 268 err = engine.runBin() 269 if err != nil { 270 t.Fatalf("Should not be fail: %s.", err) 271 } 272 } 273 274 func GetPort() (int, func()) { 275 l, err := net.Listen("tcp", ":0") 276 port := l.Addr().(*net.TCPAddr).Port 277 if err != nil { 278 panic(err) 279 } 280 return port, func() { 281 _ = l.Close() 282 } 283 } 284 285 func TestRebuild(t *testing.T) { 286 // generate a random port 287 port, f := GetPort() 288 f() 289 t.Logf("port: %d", port) 290 291 tmpDir := initTestEnv(t, port) 292 // change dir to tmpDir 293 chdir(t, tmpDir) 294 engine, err := NewEngine("", true) 295 engine.config.Build.ExcludeUnchanged = true 296 if err != nil { 297 t.Fatalf("Should not be fail: %s.", err) 298 } 299 wg := sync.WaitGroup{} 300 wg.Add(1) 301 go func() { 302 engine.Run() 303 t.Logf("engine stopped") 304 wg.Done() 305 }() 306 err = waitingPortReady(t, port, time.Second*10) 307 if err != nil { 308 t.Fatalf("Should not be fail: %s.", err) 309 } 310 t.Logf("port is ready") 311 312 // start rebuld 313 314 t.Logf("start change main.go") 315 // change file of main.go 316 // just append a new empty line to main.go 317 time.Sleep(time.Second * 2) 318 file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0o644) 319 if err != nil { 320 t.Fatalf("Should not be fail: %s.", err) 321 } 322 defer file.Close() 323 _, err = file.WriteString("\n") 324 if err != nil { 325 t.Fatalf("Should not be fail: %s.", err) 326 } 327 err = waitingPortConnectionRefused(t, port, time.Second*10) 328 if err != nil { 329 t.Fatalf("timeout: %s.", err) 330 } 331 t.Logf("connection refused") 332 time.Sleep(time.Second * 2) 333 err = waitingPortReady(t, port, time.Second*10) 334 if err != nil { 335 t.Fatalf("Should not be fail: %s.", err) 336 } 337 t.Logf("port is ready") 338 // stop engine 339 engine.Stop() 340 t.Logf("engine stopped") 341 wg.Wait() 342 time.Sleep(time.Second * 1) 343 assert.True(t, checkPortConnectionRefused(port)) 344 } 345 346 func waitingPortConnectionRefused(t *testing.T, port int, timeout time.Duration) error { 347 t.Logf("waiting port %d connection refused", port) 348 timer := time.NewTimer(timeout) 349 ticker := time.NewTicker(time.Millisecond * 100) 350 defer ticker.Stop() 351 defer timer.Stop() 352 for { 353 select { 354 case <-timer.C: 355 return fmt.Errorf("timeout") 356 case <-ticker.C: 357 print(".") 358 _, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) 359 if errors.Is(err, syscall.ECONNREFUSED) { 360 return nil 361 } 362 time.Sleep(time.Millisecond * 100) 363 } 364 } 365 } 366 367 func TestCtrlCWhenHaveKillDelay(t *testing.T) { 368 // fix https://github.com/zhb127/air/issues/278 369 // generate a random port 370 data := []byte("[build]\n kill_delay = \"2s\"") 371 c := Config{} 372 if err := toml.Unmarshal(data, &c); err != nil { 373 t.Fatalf("Should not be fail: %s.", err) 374 } 375 376 port, f := GetPort() 377 f() 378 t.Logf("port: %d", port) 379 380 tmpDir := initTestEnv(t, port) 381 // change dir to tmpDir 382 chdir(t, tmpDir) 383 engine, err := NewEngine("", true) 384 if err != nil { 385 t.Fatalf("Should not be fail: %s.", err) 386 } 387 engine.config.Build.KillDelay = c.Build.KillDelay 388 engine.config.Build.Delay = 2000 389 engine.config.Build.SendInterrupt = true 390 if err := engine.config.preprocess(); err != nil { 391 t.Fatalf("Should not be fail: %s.", err) 392 } 393 394 go func() { 395 engine.Run() 396 t.Logf("engine stopped") 397 }() 398 sigs := make(chan os.Signal, 1) 399 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 400 go func() { 401 <-sigs 402 engine.Stop() 403 t.Logf("engine stopped") 404 }() 405 if err := waitingPortReady(t, port, time.Second*10); err != nil { 406 t.Fatalf("Should not be fail: %s.", err) 407 } 408 sigs <- syscall.SIGINT 409 err = waitingPortConnectionRefused(t, port, time.Second*10) 410 if err != nil { 411 t.Fatalf("Should not be fail: %s.", err) 412 } 413 time.Sleep(time.Second * 3) 414 assert.False(t, engine.running) 415 } 416 417 func TestCtrlCWhenREngineIsRunning(t *testing.T) { 418 // generate a random port 419 port, f := GetPort() 420 f() 421 t.Logf("port: %d", port) 422 423 tmpDir := initTestEnv(t, port) 424 // change dir to tmpDir 425 chdir(t, tmpDir) 426 engine, err := NewEngine("", true) 427 if err != nil { 428 t.Fatalf("Should not be fail: %s.", err) 429 } 430 go func() { 431 engine.Run() 432 t.Logf("engine stopped") 433 }() 434 sigs := make(chan os.Signal, 1) 435 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 436 go func() { 437 <-sigs 438 engine.Stop() 439 t.Logf("engine stopped") 440 }() 441 if err := waitingPortReady(t, port, time.Second*10); err != nil { 442 t.Fatalf("Should not be fail: %s.", err) 443 } 444 sigs <- syscall.SIGINT 445 time.Sleep(time.Second * 1) 446 err = waitingPortConnectionRefused(t, port, time.Second*10) 447 if err != nil { 448 t.Fatalf("Should not be fail: %s.", err) 449 } 450 assert.False(t, engine.running) 451 } 452 453 func TestCtrlCWithFailedBin(t *testing.T) { 454 timeout := 5 * time.Second 455 done := make(chan struct{}) 456 go func() { 457 dir := initWithQuickExitGoCode(t) 458 chdir(t, dir) 459 engine, err := NewEngine("", true) 460 assert.NoError(t, err) 461 engine.config.Build.Bin = "<WRONGCOMAMND>" 462 sigs := make(chan os.Signal, 1) 463 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 464 var wg sync.WaitGroup 465 wg.Add(1) 466 go func() { 467 engine.Run() 468 t.Logf("engine stopped") 469 wg.Done() 470 }() 471 go func() { 472 <-sigs 473 engine.Stop() 474 t.Logf("engine stopped") 475 }() 476 time.Sleep(time.Second * 1) 477 sigs <- syscall.SIGINT 478 wg.Wait() 479 close(done) 480 }() 481 select { 482 case <-done: 483 case <-time.After(timeout): 484 t.Error("Test timed out") 485 } 486 } 487 488 func TestFixCloseOfChannelAfterCtrlC(t *testing.T) { 489 // fix https://github.com/zhb127/air/issues/294 490 dir := initWithBuildFailedCode(t) 491 chdir(t, dir) 492 engine, err := NewEngine("", true) 493 if err != nil { 494 t.Fatalf("Should not be fail: %s.", err) 495 } 496 sigs := make(chan os.Signal, 1) 497 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 498 go func() { 499 engine.Run() 500 t.Logf("engine stopped") 501 }() 502 503 go func() { 504 <-sigs 505 engine.Stop() 506 t.Logf("engine stopped") 507 }() 508 // waiting for compile error 509 time.Sleep(time.Second * 3) 510 port, f := GetPort() 511 f() 512 // correct code 513 err = generateGoCode(dir, port) 514 if err != nil { 515 t.Fatalf("Should not be fail: %s.", err) 516 } 517 518 if err := waitingPortReady(t, port, time.Second*10); err != nil { 519 t.Fatalf("Should not be fail: %s.", err) 520 } 521 522 // ctrl + c 523 sigs <- syscall.SIGINT 524 time.Sleep(time.Second * 1) 525 if err := waitingPortConnectionRefused(t, port, time.Second*10); err != nil { 526 t.Fatalf("Should not be fail: %s.", err) 527 } 528 assert.False(t, engine.running) 529 } 530 531 func TestFixCloseOfChannelAfterTwoFailedBuild(t *testing.T) { 532 // fix https://github.com/zhb127/air/issues/294 533 // happens after two failed builds 534 dir := initWithBuildFailedCode(t) 535 // change dir to tmpDir 536 chdir(t, dir) 537 engine, err := NewEngine("", true) 538 if err != nil { 539 t.Fatalf("Should not be fail: %s.", err) 540 } 541 sigs := make(chan os.Signal, 1) 542 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 543 go func() { 544 engine.Run() 545 t.Logf("engine stopped") 546 }() 547 548 go func() { 549 <-sigs 550 engine.Stop() 551 t.Logf("engine stopped") 552 }() 553 554 // waiting for compile error 555 time.Sleep(time.Second * 3) 556 557 // edit *.go file to create build error again 558 file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0o644) 559 if err != nil { 560 t.Fatalf("Should not be fail: %s.", err) 561 } 562 defer file.Close() 563 _, err = file.WriteString("\n") 564 if err != nil { 565 t.Fatalf("Should not be fail: %s.", err) 566 } 567 time.Sleep(time.Second * 3) 568 // ctrl + c 569 sigs <- syscall.SIGINT 570 time.Sleep(time.Second * 1) 571 assert.False(t, engine.running) 572 } 573 574 // waitingPortReady waits until the port is ready to be used. 575 func waitingPortReady(t *testing.T, port int, timeout time.Duration) error { 576 t.Logf("waiting port %d ready", port) 577 timeoutChan := time.After(timeout) 578 ticker := time.NewTicker(time.Millisecond * 100) 579 defer ticker.Stop() 580 for { 581 select { 582 case <-timeoutChan: 583 return fmt.Errorf("timeout") 584 case <-ticker.C: 585 conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) 586 if err == nil { 587 _ = conn.Close() 588 return nil 589 } 590 } 591 } 592 } 593 594 func TestRun(t *testing.T) { 595 // generate a random port 596 port, f := GetPort() 597 f() 598 t.Logf("port: %d", port) 599 600 tmpDir := initTestEnv(t, port) 601 // change dir to tmpDir 602 chdir(t, tmpDir) 603 engine, err := NewEngine("", true) 604 if err != nil { 605 t.Fatalf("Should not be fail: %s.", err) 606 } 607 608 go func() { 609 engine.Run() 610 }() 611 time.Sleep(time.Second * 2) 612 assert.True(t, checkPortHaveBeenUsed(port)) 613 t.Logf("try to stop") 614 engine.Stop() 615 time.Sleep(time.Second * 1) 616 assert.False(t, checkPortHaveBeenUsed(port)) 617 t.Logf("stoped") 618 } 619 620 func checkPortConnectionRefused(port int) bool { 621 conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) 622 defer func() { 623 if conn != nil { 624 _ = conn.Close() 625 } 626 }() 627 return errors.Is(err, syscall.ECONNREFUSED) 628 } 629 630 func checkPortHaveBeenUsed(port int) bool { 631 conn, err := net.Dial("tcp", fmt.Sprintf(":%d", port)) 632 if err != nil { 633 return false 634 } 635 _ = conn.Close() 636 return true 637 } 638 639 func initTestEnv(t *testing.T, port int) string { 640 tempDir := t.TempDir() 641 t.Logf("tempDir: %s", tempDir) 642 // generate golang code to tempdir 643 err := generateGoCode(tempDir, port) 644 if err != nil { 645 t.Fatalf("Should not be fail: %s.", err) 646 } 647 return tempDir 648 } 649 650 func initWithBuildFailedCode(t *testing.T) string { 651 tempDir := t.TempDir() 652 t.Logf("tempDir: %s", tempDir) 653 // generate golang code to tempdir 654 err := generateBuildErrorGoCode(tempDir) 655 if err != nil { 656 t.Fatalf("Should not be fail: %s.", err) 657 } 658 return tempDir 659 } 660 661 func initWithQuickExitGoCode(t *testing.T) string { 662 tempDir := t.TempDir() 663 t.Logf("tempDir: %s", tempDir) 664 // generate golang code to tempdir 665 err := generateQuickExitGoCode(tempDir) 666 if err != nil { 667 t.Fatalf("Should not be fail: %s.", err) 668 } 669 return tempDir 670 } 671 672 func generateQuickExitGoCode(dir string) error { 673 code := `package main 674 // You can edit this code! 675 // Click here and start typing. 676 677 import "fmt" 678 679 func main() { 680 fmt.Println("Hello, 世界") 681 } 682 ` 683 file, err := os.Create(dir + "/main.go") 684 if err != nil { 685 return err 686 } 687 _, err = file.WriteString(code) 688 if err != nil { 689 return err 690 } 691 692 // generate go mod file 693 mod := `module air.sample.com 694 695 go 1.17 696 ` 697 file, err = os.Create(dir + "/go.mod") 698 if err != nil { 699 return err 700 } 701 _, err = file.WriteString(mod) 702 if err != nil { 703 return err 704 } 705 return nil 706 } 707 708 func generateBuildErrorGoCode(dir string) error { 709 code := `package main 710 // You can edit this code! 711 // Click here and start typing. 712 713 func main() { 714 Println("Hello, 世界") 715 716 } 717 ` 718 file, err := os.Create(dir + "/main.go") 719 if err != nil { 720 return err 721 } 722 _, err = file.WriteString(code) 723 if err != nil { 724 return err 725 } 726 727 // generate go mod file 728 mod := `module air.sample.com 729 730 go 1.17 731 ` 732 file, err = os.Create(dir + "/go.mod") 733 if err != nil { 734 return err 735 } 736 _, err = file.WriteString(mod) 737 if err != nil { 738 return err 739 } 740 return nil 741 } 742 743 // generateGoCode generates golang code to tempdir 744 func generateGoCode(dir string, port int) error { 745 code := fmt.Sprintf(`package main 746 747 import ( 748 "log" 749 "net/http" 750 ) 751 752 func main() { 753 log.Fatal(http.ListenAndServe(":%v", nil)) 754 } 755 `, port) 756 file, err := os.Create(dir + "/main.go") 757 if err != nil { 758 return err 759 } 760 _, err = file.WriteString(code) 761 if err != nil { 762 return err 763 } 764 765 // generate go mod file 766 mod := `module air.sample.com 767 768 go 1.17 769 ` 770 file, err = os.Create(dir + "/go.mod") 771 if err != nil { 772 return err 773 } 774 _, err = file.WriteString(mod) 775 if err != nil { 776 return err 777 } 778 return nil 779 } 780 781 func TestRebuildWhenRunCmdUsingDLV(t *testing.T) { 782 // generate a random port 783 port, f := GetPort() 784 f() 785 t.Logf("port: %d", port) 786 tmpDir := initTestEnv(t, port) 787 // change dir to tmpDir 788 chdir(t, tmpDir) 789 engine, err := NewEngine("", true) 790 if err != nil { 791 t.Fatalf("Should not be fail: %s.", err) 792 } 793 engine.config.Build.Cmd = "go build -gcflags='all=-N -l' -o ./tmp/main ." 794 engine.config.Build.Bin = "" 795 dlvPort, f := GetPort() 796 f() 797 engine.config.Build.FullBin = fmt.Sprintf("dlv exec --accept-multiclient --log --headless --continue --listen :%d --api-version 2 ./tmp/main", dlvPort) 798 _ = engine.config.preprocess() 799 go func() { 800 engine.Run() 801 }() 802 if err := waitingPortReady(t, port, time.Second*40); err != nil { 803 t.Fatalf("Should not be fail: %s.", err) 804 } 805 806 t.Logf("start change main.go") 807 // change file of main.go 808 // just append a new empty line to main.go 809 time.Sleep(time.Second * 2) 810 go func() { 811 file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0o644) 812 if err != nil { 813 log.Fatalf("Should not be fail: %s.", err) 814 } 815 defer file.Close() 816 _, err = file.WriteString("\n") 817 if err != nil { 818 log.Fatalf("Should not be fail: %s.", err) 819 } 820 }() 821 err = waitingPortConnectionRefused(t, port, time.Second*10) 822 if err != nil { 823 t.Fatalf("timeout: %s.", err) 824 } 825 t.Logf("connection refused") 826 time.Sleep(time.Second * 2) 827 err = waitingPortReady(t, port, time.Second*40) 828 if err != nil { 829 t.Fatalf("Should not be fail: %s.", err) 830 } 831 t.Logf("port is ready") 832 // stop engine 833 engine.Stop() 834 time.Sleep(time.Second * 3) 835 t.Logf("engine stopped") 836 assert.True(t, checkPortConnectionRefused(port)) 837 } 838 839 func TestWriteDefaultConfig(t *testing.T) { 840 port, f := GetPort() 841 f() 842 t.Logf("port: %d", port) 843 844 tmpDir := initTestEnv(t, port) 845 // change dir to tmpDir 846 chdir(t, tmpDir) 847 configName, err := writeDefaultConfig() 848 if err != nil { 849 t.Fatal(err) 850 } 851 // check the file is exist 852 if _, err := os.Stat(configName); err != nil { 853 t.Fatal(err) 854 } 855 856 // check the file content is right 857 actual, err := readConfig(configName) 858 if err != nil { 859 t.Fatal(err) 860 } 861 expect := defaultConfig() 862 863 assert.Equal(t, expect, *actual) 864 } 865 866 func TestCheckNilSliceShouldBeenOverwrite(t *testing.T) { 867 port, f := GetPort() 868 f() 869 t.Logf("port: %d", port) 870 871 tmpDir := initTestEnv(t, port) 872 873 // change dir to tmpDir 874 chdir(t, tmpDir) 875 876 // write easy config file 877 878 config := ` 879 [build] 880 cmd = "go build ." 881 bin = "tmp/main" 882 exclude_regex = [] 883 exclude_dir = ["test"] 884 exclude_file = ["main.go"] 885 include_file = ["test/not_a_test.go"] 886 887 ` 888 if err := os.WriteFile(dftTOML, []byte(config), 0o644); err != nil { 889 t.Fatal(err) 890 } 891 engine, err := NewEngine(".air.toml", true) 892 if err != nil { 893 t.Fatal(err) 894 } 895 assert.Equal(t, []string{"go", "tpl", "tmpl", "html"}, engine.config.Build.IncludeExt) 896 assert.Equal(t, []string{}, engine.config.Build.ExcludeRegex) 897 assert.Equal(t, []string{"test"}, engine.config.Build.ExcludeDir) 898 // add new config 899 assert.Equal(t, []string{"main.go"}, engine.config.Build.ExcludeFile) 900 assert.Equal(t, []string{"test/not_a_test.go"}, engine.config.Build.IncludeFile) 901 assert.Equal(t, "go build .", engine.config.Build.Cmd) 902 } 903 904 func TestShouldIncludeGoTestFile(t *testing.T) { 905 port, f := GetPort() 906 f() 907 t.Logf("port: %d", port) 908 909 tmpDir := initTestEnv(t, port) 910 // change dir to tmpDir 911 chdir(t, tmpDir) 912 _, err := writeDefaultConfig() 913 if err != nil { 914 t.Fatal(err) 915 } 916 917 // write go test file 918 file, err := os.Create("main_test.go") 919 if err != nil { 920 t.Fatal(err) 921 } 922 _, err = file.WriteString(`package main 923 924 import "testing" 925 926 func Test(t *testing.T) { 927 t.Log("testing") 928 } 929 `) 930 // run sed 931 // check the file is exist 932 if _, err := os.Stat(dftTOML); err != nil { 933 t.Fatal(err) 934 } 935 // check is MacOS 936 var cmd *exec.Cmd 937 if runtime.GOOS == "darwin" { 938 cmd = exec.Command("gsed", "-i", "s/\"_test.*go\"//g", ".air.toml") 939 } else { 940 cmd = exec.Command("sed", "-i", "s/\"_test.*go\"//g", ".air.toml") 941 } 942 cmd.Stdout = os.Stdout 943 cmd.Stderr = os.Stderr 944 if err := cmd.Run(); err != nil { 945 t.Fatal(err) 946 } 947 948 time.Sleep(time.Second * 2) 949 engine, err := NewEngine(".air.toml", false) 950 if err != nil { 951 t.Fatal(err) 952 } 953 go func() { 954 engine.Run() 955 }() 956 957 t.Logf("start change main_test.go") 958 // change file of main_test.go 959 // just append a new empty line to main_test.go 960 if err = waitingPortReady(t, port, time.Second*40); err != nil { 961 t.Fatal(err) 962 } 963 go func() { 964 file, err = os.OpenFile("main_test.go", os.O_APPEND|os.O_WRONLY, 0o644) 965 assert.NoError(t, err) 966 defer file.Close() 967 _, err = file.WriteString("\n") 968 assert.NoError(t, err) 969 }() 970 // should Have rebuild 971 if err = waitingPortReady(t, port, time.Second*10); err != nil { 972 t.Fatal(err) 973 } 974 } 975 976 func TestCreateNewDir(t *testing.T) { 977 // generate a random port 978 port, f := GetPort() 979 f() 980 t.Logf("port: %d", port) 981 982 tmpDir := initTestEnv(t, port) 983 // change dir to tmpDir 984 chdir(t, tmpDir) 985 engine, err := NewEngine("", true) 986 if err != nil { 987 t.Fatalf("Should not be fail: %s.", err) 988 } 989 990 go func() { 991 engine.Run() 992 }() 993 time.Sleep(time.Second * 2) 994 assert.True(t, checkPortHaveBeenUsed(port)) 995 996 // create a new dir make dir 997 if err = os.Mkdir(tmpDir+"/dir", 0o644); err != nil { 998 t.Fatal(err) 999 } 1000 1001 // no need reload 1002 if err = waitingPortConnectionRefused(t, port, 3*time.Second); err == nil { 1003 t.Fatal("should raise a error") 1004 } 1005 engine.Stop() 1006 time.Sleep(2 * time.Second) 1007 } 1008 1009 func TestShouldIncludeIncludedFile(t *testing.T) { 1010 port, f := GetPort() 1011 f() 1012 t.Logf("port: %d", port) 1013 1014 tmpDir := initTestEnv(t, port) 1015 1016 chdir(t, tmpDir) 1017 1018 config := ` 1019 [build] 1020 cmd = "true" # do nothing 1021 full_bin = "sh main.sh" 1022 include_ext = [""] 1023 include_dir = ["nonexist"] # prevent default "." watch from taking effect 1024 include_file = ["main.sh"] 1025 ` 1026 if err := os.WriteFile(dftTOML, []byte(config), 0o644); err != nil { 1027 t.Fatal(err) 1028 } 1029 1030 err := os.WriteFile("main.sh", []byte("#!/bin/sh\nprintf original > output"), 0o755) 1031 if err != nil { 1032 t.Fatal(err) 1033 } 1034 1035 engine, err := NewEngine(dftTOML, false) 1036 if err != nil { 1037 t.Fatal(err) 1038 } 1039 go func() { 1040 engine.Run() 1041 }() 1042 1043 time.Sleep(time.Second * 1) 1044 1045 bytes, err := os.ReadFile("output") 1046 if err != nil { 1047 t.Fatal(err) 1048 } 1049 assert.Equal(t, []byte("original"), bytes) 1050 1051 t.Logf("start change main.sh") 1052 go func() { 1053 err := os.WriteFile("main.sh", []byte("#!/bin/sh\nprintf modified > output"), 0o755) 1054 if err != nil { 1055 log.Fatalf("Error updating file: %s.", err) 1056 } 1057 }() 1058 1059 time.Sleep(time.Second * 3) 1060 1061 bytes, err = os.ReadFile("output") 1062 if err != nil { 1063 t.Fatal(err) 1064 } 1065 assert.Equal(t, []byte("modified"), bytes) 1066 }