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