github.com/bilpay-tech/air@v0.0.0-20230514155040-b55f770a4ac6/runner/engine_test.go (about)

     1  package runner
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"net"
     9  	"os"
    10  	"os/exec"
    11  	"os/signal"
    12  	"runtime"
    13  	"strings"
    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 TestRunBin(t *testing.T) {
   189  	engine, err := NewEngine("", true)
   190  	if err != nil {
   191  		t.Fatalf("Should not be fail: %s.", err)
   192  	}
   193  
   194  	err = engine.runBin()
   195  	if err != nil {
   196  		t.Fatalf("Should not be fail: %s.", err)
   197  	}
   198  }
   199  
   200  func GetPort() (int, func()) {
   201  	l, err := net.Listen("tcp", ":0")
   202  	port := l.Addr().(*net.TCPAddr).Port
   203  	if err != nil {
   204  		panic(err)
   205  	}
   206  	return port, func() {
   207  		_ = l.Close()
   208  	}
   209  }
   210  
   211  func TestRebuild(t *testing.T) {
   212  	// generate a random port
   213  	port, f := GetPort()
   214  	f()
   215  	t.Logf("port: %d", port)
   216  
   217  	tmpDir := initTestEnv(t, port)
   218  	// change dir to tmpDir
   219  	chdir(t, tmpDir)
   220  	engine, err := NewEngine("", true)
   221  	engine.config.Build.ExcludeUnchanged = true
   222  	if err != nil {
   223  		t.Fatalf("Should not be fail: %s.", err)
   224  	}
   225  	go func() {
   226  		engine.Run()
   227  		t.Logf("engine stopped")
   228  	}()
   229  	err = waitingPortReady(t, port, time.Second*10)
   230  	if err != nil {
   231  		t.Fatalf("Should not be fail: %s.", err)
   232  	}
   233  	t.Logf("port is ready")
   234  
   235  	// start rebuld
   236  
   237  	t.Logf("start change main.go")
   238  	// change file of main.go
   239  	// just append a new empty line to main.go
   240  	time.Sleep(time.Second * 2)
   241  	file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0o644)
   242  	if err != nil {
   243  		t.Fatalf("Should not be fail: %s.", err)
   244  	}
   245  	defer file.Close()
   246  	_, err = file.WriteString("\n")
   247  	if err != nil {
   248  		t.Fatalf("Should not be fail: %s.", err)
   249  	}
   250  	err = waitingPortConnectionRefused(t, port, time.Second*10)
   251  	if err != nil {
   252  		t.Fatalf("timeout: %s.", err)
   253  	}
   254  	t.Logf("connection refused")
   255  	time.Sleep(time.Second * 2)
   256  	err = waitingPortReady(t, port, time.Second*10)
   257  	if err != nil {
   258  		t.Fatalf("Should not be fail: %s.", err)
   259  	}
   260  	t.Logf("port is ready")
   261  	// stop engine
   262  	engine.Stop()
   263  	time.Sleep(time.Second * 1)
   264  	t.Logf("engine stopped")
   265  	assert.True(t, checkPortConnectionRefused(port))
   266  }
   267  
   268  func waitingPortConnectionRefused(t *testing.T, port int, timeout time.Duration) error {
   269  	t.Logf("waiting port %d connection refused", port)
   270  	timer := time.NewTimer(timeout)
   271  	ticker := time.NewTicker(time.Millisecond * 100)
   272  	defer ticker.Stop()
   273  	defer timer.Stop()
   274  	for {
   275  		select {
   276  		case <-timer.C:
   277  			return fmt.Errorf("timeout")
   278  		case <-ticker.C:
   279  			print(".")
   280  			_, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
   281  			if errors.Is(err, syscall.ECONNREFUSED) {
   282  				return nil
   283  			}
   284  			time.Sleep(time.Millisecond * 100)
   285  		}
   286  	}
   287  }
   288  
   289  func TestCtrlCWhenHaveKillDelay(t *testing.T) {
   290  	// fix https://github.com/bilpay-tech/air/issues/278
   291  	// generate a random port
   292  	data := []byte("[build]\n  kill_delay = \"2s\"")
   293  	c := Config{}
   294  	if err := toml.Unmarshal(data, &c); err != nil {
   295  		t.Fatalf("Should not be fail: %s.", err)
   296  	}
   297  
   298  	port, f := GetPort()
   299  	f()
   300  	t.Logf("port: %d", port)
   301  
   302  	tmpDir := initTestEnv(t, port)
   303  	// change dir to tmpDir
   304  	chdir(t, tmpDir)
   305  	engine, err := NewEngine("", true)
   306  	if err != nil {
   307  		t.Fatalf("Should not be fail: %s.", err)
   308  	}
   309  	engine.config.Build.KillDelay = c.Build.KillDelay
   310  	engine.config.Build.Delay = 2000
   311  	engine.config.Build.SendInterrupt = true
   312  	engine.config.preprocess()
   313  
   314  	go func() {
   315  		engine.Run()
   316  		t.Logf("engine stopped")
   317  	}()
   318  	sigs := make(chan os.Signal, 1)
   319  	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
   320  	go func() {
   321  		<-sigs
   322  		engine.Stop()
   323  		t.Logf("engine stopped")
   324  	}()
   325  	if err := waitingPortReady(t, port, time.Second*10); err != nil {
   326  		t.Fatalf("Should not be fail: %s.", err)
   327  	}
   328  	sigs <- syscall.SIGINT
   329  	err = waitingPortConnectionRefused(t, port, time.Second*10)
   330  	if err != nil {
   331  		t.Fatalf("Should not be fail: %s.", err)
   332  	}
   333  	time.Sleep(time.Second * 3)
   334  	assert.False(t, engine.running)
   335  }
   336  
   337  func TestCtrlCWhenREngineIsRunning(t *testing.T) {
   338  	// generate a random port
   339  	port, f := GetPort()
   340  	f()
   341  	t.Logf("port: %d", port)
   342  
   343  	tmpDir := initTestEnv(t, port)
   344  	// change dir to tmpDir
   345  	chdir(t, tmpDir)
   346  	engine, err := NewEngine("", true)
   347  	if err != nil {
   348  		t.Fatalf("Should not be fail: %s.", err)
   349  	}
   350  	go func() {
   351  		engine.Run()
   352  		t.Logf("engine stopped")
   353  	}()
   354  	sigs := make(chan os.Signal, 1)
   355  	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
   356  	go func() {
   357  		<-sigs
   358  		engine.Stop()
   359  		t.Logf("engine stopped")
   360  	}()
   361  	if err := waitingPortReady(t, port, time.Second*10); err != nil {
   362  		t.Fatalf("Should not be fail: %s.", err)
   363  	}
   364  	sigs <- syscall.SIGINT
   365  	time.Sleep(time.Second * 1)
   366  	err = waitingPortConnectionRefused(t, port, time.Second*10)
   367  	if err != nil {
   368  		t.Fatalf("Should not be fail: %s.", err)
   369  	}
   370  	assert.False(t, engine.running)
   371  }
   372  
   373  func TestFixCloseOfChannelAfterCtrlC(t *testing.T) {
   374  	// fix https://github.com/bilpay-tech/air/issues/294
   375  	dir := initWithBuildFailedCode(t)
   376  	chdir(t, dir)
   377  	engine, err := NewEngine("", true)
   378  	if err != nil {
   379  		t.Fatalf("Should not be fail: %s.", err)
   380  	}
   381  	sigs := make(chan os.Signal, 1)
   382  	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
   383  	go func() {
   384  		engine.Run()
   385  		t.Logf("engine stopped")
   386  	}()
   387  
   388  	go func() {
   389  		<-sigs
   390  		engine.Stop()
   391  		t.Logf("engine stopped")
   392  	}()
   393  	// waiting for compile error
   394  	time.Sleep(time.Second * 3)
   395  	port, f := GetPort()
   396  	f()
   397  	// correct code
   398  	err = generateGoCode(dir, port)
   399  	if err != nil {
   400  		t.Fatalf("Should not be fail: %s.", err)
   401  	}
   402  
   403  	if err := waitingPortReady(t, port, time.Second*10); err != nil {
   404  		t.Fatalf("Should not be fail: %s.", err)
   405  	}
   406  
   407  	// ctrl + c
   408  	sigs <- syscall.SIGINT
   409  	time.Sleep(time.Second * 1)
   410  	if err := waitingPortConnectionRefused(t, port, time.Second*10); err != nil {
   411  		t.Fatalf("Should not be fail: %s.", err)
   412  	}
   413  	assert.False(t, engine.running)
   414  }
   415  
   416  func TestFixCloseOfChannelAfterTwoFailedBuild(t *testing.T) {
   417  	// fix https://github.com/bilpay-tech/air/issues/294
   418  	// happens after two failed builds
   419  	dir := initWithBuildFailedCode(t)
   420  	// change dir to tmpDir
   421  	chdir(t, dir)
   422  	engine, err := NewEngine("", true)
   423  	if err != nil {
   424  		t.Fatalf("Should not be fail: %s.", err)
   425  	}
   426  	sigs := make(chan os.Signal, 1)
   427  	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
   428  	go func() {
   429  		engine.Run()
   430  		t.Logf("engine stopped")
   431  	}()
   432  
   433  	go func() {
   434  		<-sigs
   435  		engine.Stop()
   436  		t.Logf("engine stopped")
   437  	}()
   438  
   439  	// waiting for compile error
   440  	time.Sleep(time.Second * 3)
   441  
   442  	// edit *.go file to create build error again
   443  	file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0o644)
   444  	if err != nil {
   445  		t.Fatalf("Should not be fail: %s.", err)
   446  	}
   447  	defer file.Close()
   448  	_, err = file.WriteString("\n")
   449  	if err != nil {
   450  		t.Fatalf("Should not be fail: %s.", err)
   451  	}
   452  	time.Sleep(time.Second * 3)
   453  	// ctrl + c
   454  	sigs <- syscall.SIGINT
   455  	time.Sleep(time.Second * 1)
   456  	assert.False(t, engine.running)
   457  }
   458  
   459  // waitingPortReady waits until the port is ready to be used.
   460  func waitingPortReady(t *testing.T, port int, timeout time.Duration) error {
   461  	t.Logf("waiting port %d ready", port)
   462  	timeoutChan := time.After(timeout)
   463  	ticker := time.NewTicker(time.Millisecond * 100)
   464  	defer ticker.Stop()
   465  	for {
   466  		select {
   467  		case <-timeoutChan:
   468  			return fmt.Errorf("timeout")
   469  		case <-ticker.C:
   470  			conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
   471  			if err == nil {
   472  				_ = conn.Close()
   473  				return nil
   474  			}
   475  		}
   476  	}
   477  }
   478  
   479  func TestRun(t *testing.T) {
   480  	// generate a random port
   481  	port, f := GetPort()
   482  	f()
   483  	t.Logf("port: %d", port)
   484  
   485  	tmpDir := initTestEnv(t, port)
   486  	// change dir to tmpDir
   487  	chdir(t, tmpDir)
   488  	engine, err := NewEngine("", true)
   489  	if err != nil {
   490  		t.Fatalf("Should not be fail: %s.", err)
   491  	}
   492  
   493  	go func() {
   494  		engine.Run()
   495  	}()
   496  	time.Sleep(time.Second * 2)
   497  	assert.True(t, checkPortHaveBeenUsed(port))
   498  	t.Logf("try to stop")
   499  	engine.Stop()
   500  	time.Sleep(time.Second * 1)
   501  	assert.False(t, checkPortHaveBeenUsed(port))
   502  	t.Logf("stoped")
   503  }
   504  
   505  func checkPortConnectionRefused(port int) bool {
   506  	conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
   507  	defer func() {
   508  		if conn != nil {
   509  			_ = conn.Close()
   510  		}
   511  	}()
   512  	return errors.Is(err, syscall.ECONNREFUSED)
   513  }
   514  
   515  func checkPortHaveBeenUsed(port int) bool {
   516  	conn, err := net.Dial("tcp", fmt.Sprintf(":%d", port))
   517  	if err != nil {
   518  		return false
   519  	}
   520  	_ = conn.Close()
   521  	return true
   522  }
   523  
   524  func initTestEnv(t *testing.T, port int) string {
   525  	tempDir := t.TempDir()
   526  	t.Logf("tempDir: %s", tempDir)
   527  	// generate golang code to tempdir
   528  	err := generateGoCode(tempDir, port)
   529  	if err != nil {
   530  		t.Fatalf("Should not be fail: %s.", err)
   531  	}
   532  	return tempDir
   533  }
   534  
   535  func initWithBuildFailedCode(t *testing.T) string {
   536  	tempDir := t.TempDir()
   537  	t.Logf("tempDir: %s", tempDir)
   538  	// generate golang code to tempdir
   539  	err := generateBuildErrorGoCode(tempDir)
   540  	if err != nil {
   541  		t.Fatalf("Should not be fail: %s.", err)
   542  	}
   543  	return tempDir
   544  }
   545  
   546  func initWithQuickExitGoCode(t *testing.T) string {
   547  	tempDir := t.TempDir()
   548  	t.Logf("tempDir: %s", tempDir)
   549  	// generate golang code to tempdir
   550  	err := generateQuickExitGoCode(tempDir)
   551  	if err != nil {
   552  		t.Fatalf("Should not be fail: %s.", err)
   553  	}
   554  	return tempDir
   555  }
   556  
   557  func generateQuickExitGoCode(dir string) error {
   558  	code := `package main
   559  // You can edit this code!
   560  // Click here and start typing.
   561  
   562  import "fmt"
   563  
   564  func main() {
   565  	fmt.Println("Hello, 世界")
   566  }
   567  `
   568  	file, err := os.Create(dir + "/main.go")
   569  	if err != nil {
   570  		return err
   571  	}
   572  	_, err = file.WriteString(code)
   573  	if err != nil {
   574  		return err
   575  	}
   576  
   577  	// generate go mod file
   578  	mod := `module air.sample.com
   579  
   580  go 1.17
   581  `
   582  	file, err = os.Create(dir + "/go.mod")
   583  	if err != nil {
   584  		return err
   585  	}
   586  	_, err = file.WriteString(mod)
   587  	if err != nil {
   588  		return err
   589  	}
   590  	return nil
   591  }
   592  
   593  func generateBuildErrorGoCode(dir string) error {
   594  	code := `package main
   595  // You can edit this code!
   596  // Click here and start typing.
   597  
   598  func main() {
   599  	Println("Hello, 世界")
   600  
   601  }
   602  `
   603  	file, err := os.Create(dir + "/main.go")
   604  	if err != nil {
   605  		return err
   606  	}
   607  	_, err = file.WriteString(code)
   608  	if err != nil {
   609  		return err
   610  	}
   611  
   612  	// generate go mod file
   613  	mod := `module air.sample.com
   614  
   615  go 1.17
   616  `
   617  	file, err = os.Create(dir + "/go.mod")
   618  	if err != nil {
   619  		return err
   620  	}
   621  	_, err = file.WriteString(mod)
   622  	if err != nil {
   623  		return err
   624  	}
   625  	return nil
   626  }
   627  
   628  // generateGoCode generates golang code to tempdir
   629  func generateGoCode(dir string, port int) error {
   630  	code := fmt.Sprintf(`package main
   631  
   632  import (
   633  	"log"
   634  	"net/http"
   635  )
   636  
   637  func main() {
   638  	log.Fatal(http.ListenAndServe(":%v", nil))
   639  }
   640  `, port)
   641  	file, err := os.Create(dir + "/main.go")
   642  	if err != nil {
   643  		return err
   644  	}
   645  	_, err = file.WriteString(code)
   646  	if err != nil {
   647  		return err
   648  	}
   649  
   650  	// generate go mod file
   651  	mod := `module air.sample.com
   652  
   653  go 1.17
   654  `
   655  	file, err = os.Create(dir + "/go.mod")
   656  	if err != nil {
   657  		return err
   658  	}
   659  	_, err = file.WriteString(mod)
   660  	if err != nil {
   661  		return err
   662  	}
   663  	return nil
   664  }
   665  
   666  func TestRebuildWhenRunCmdUsingDLV(t *testing.T) {
   667  	// generate a random port
   668  	port, f := GetPort()
   669  	f()
   670  	t.Logf("port: %d", port)
   671  	tmpDir := initTestEnv(t, port)
   672  	// change dir to tmpDir
   673  	chdir(t, tmpDir)
   674  	engine, err := NewEngine("", true)
   675  	if err != nil {
   676  		t.Fatalf("Should not be fail: %s.", err)
   677  	}
   678  	engine.config.Build.Cmd = "go build -gcflags='all=-N -l' -o ./tmp/main ."
   679  	engine.config.Build.Bin = ""
   680  	dlvPort, f := GetPort()
   681  	f()
   682  	engine.config.Build.FullBin = fmt.Sprintf("dlv exec --accept-multiclient --log --headless --continue --listen :%d --api-version 2 ./tmp/main", dlvPort)
   683  	_ = engine.config.preprocess()
   684  	go func() {
   685  		engine.Run()
   686  	}()
   687  	if err := waitingPortReady(t, port, time.Second*40); err != nil {
   688  		t.Fatalf("Should not be fail: %s.", err)
   689  	}
   690  
   691  	t.Logf("start change main.go")
   692  	// change file of main.go
   693  	// just append a new empty line to main.go
   694  	time.Sleep(time.Second * 2)
   695  	go func() {
   696  		file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0o644)
   697  		if err != nil {
   698  			log.Fatalf("Should not be fail: %s.", err)
   699  		}
   700  		defer file.Close()
   701  		_, err = file.WriteString("\n")
   702  		if err != nil {
   703  			log.Fatalf("Should not be fail: %s.", err)
   704  		}
   705  	}()
   706  	err = waitingPortConnectionRefused(t, port, time.Second*10)
   707  	if err != nil {
   708  		t.Fatalf("timeout: %s.", err)
   709  	}
   710  	t.Logf("connection refused")
   711  	time.Sleep(time.Second * 2)
   712  	err = waitingPortReady(t, port, time.Second*40)
   713  	if err != nil {
   714  		t.Fatalf("Should not be fail: %s.", err)
   715  	}
   716  	t.Logf("port is ready")
   717  	// stop engine
   718  	engine.Stop()
   719  	time.Sleep(time.Second * 3)
   720  	t.Logf("engine stopped")
   721  	assert.True(t, checkPortConnectionRefused(port))
   722  }
   723  
   724  func TestWriteDefaultConfig(t *testing.T) {
   725  	port, f := GetPort()
   726  	f()
   727  	t.Logf("port: %d", port)
   728  
   729  	tmpDir := initTestEnv(t, port)
   730  	// change dir to tmpDir
   731  	chdir(t, tmpDir)
   732  	writeDefaultConfig()
   733  	// check the file is exist
   734  	if _, err := os.Stat(dftTOML); err != nil {
   735  		t.Fatal(err)
   736  	}
   737  
   738  	// check the file content is right
   739  	actual, err := readConfig(dftTOML)
   740  	if err != nil {
   741  		t.Fatal(err)
   742  	}
   743  	expect := defaultConfig()
   744  
   745  	assert.Equal(t, expect, *actual)
   746  }
   747  
   748  func TestCheckNilSliceShouldBeenOverwrite(t *testing.T) {
   749  	port, f := GetPort()
   750  	f()
   751  	t.Logf("port: %d", port)
   752  
   753  	tmpDir := initTestEnv(t, port)
   754  
   755  	// change dir to tmpDir
   756  	chdir(t, tmpDir)
   757  
   758  	// write easy config file
   759  
   760  	config := `
   761  [build]
   762  cmd = "go build ."
   763  bin = "tmp/main"
   764  exclude_regex = []
   765  exclude_dir = ["test"]
   766  exclude_file = ["main.go"]
   767  include_file = ["test/not_a_test.go"]
   768  
   769  `
   770  	if err := ioutil.WriteFile(dftTOML, []byte(config), 0o644); err != nil {
   771  		t.Fatal(err)
   772  	}
   773  	engine, err := NewEngine(".air.toml", true)
   774  	if err != nil {
   775  		t.Fatal(err)
   776  	}
   777  	assert.Equal(t, []string{"go", "tpl", "tmpl", "html"}, engine.config.Build.IncludeExt)
   778  	assert.Equal(t, []string{}, engine.config.Build.ExcludeRegex)
   779  	assert.Equal(t, []string{"test"}, engine.config.Build.ExcludeDir)
   780  	// add new config
   781  	assert.Equal(t, []string{"main.go"}, engine.config.Build.ExcludeFile)
   782  	assert.Equal(t, []string{"test/not_a_test.go"}, engine.config.Build.IncludeFile)
   783  	assert.Equal(t, "go build .", engine.config.Build.Cmd)
   784  }
   785  
   786  func TestShouldIncludeGoTestFile(t *testing.T) {
   787  	port, f := GetPort()
   788  	f()
   789  	t.Logf("port: %d", port)
   790  
   791  	tmpDir := initTestEnv(t, port)
   792  	// change dir to tmpDir
   793  	chdir(t, tmpDir)
   794  	writeDefaultConfig()
   795  
   796  	// write go test file
   797  	file, err := os.Create("main_test.go")
   798  	if err != nil {
   799  		t.Fatal(err)
   800  	}
   801  	_, err = file.WriteString(`package main
   802  
   803  import "testing"
   804  
   805  func Test(t *testing.T) {
   806  	t.Log("testing")
   807  }
   808  `)
   809  	// run sed
   810  	// check the file is exist
   811  	if _, err := os.Stat(dftTOML); err != nil {
   812  		t.Fatal(err)
   813  	}
   814  	// check is MacOS
   815  	var cmd *exec.Cmd
   816  	if runtime.GOOS == "darwin" {
   817  		cmd = exec.Command("gsed", "-i", "s/\"_test.*go\"//g", ".air.toml")
   818  	} else {
   819  		cmd = exec.Command("sed", "-i", "s/\"_test.*go\"//g", ".air.toml")
   820  	}
   821  	cmd.Stdout = os.Stdout
   822  	cmd.Stderr = os.Stderr
   823  	if err := cmd.Run(); err != nil {
   824  		t.Fatal(err)
   825  	}
   826  
   827  	time.Sleep(time.Second * 2)
   828  	engine, err := NewEngine(".air.toml", false)
   829  	if err != nil {
   830  		t.Fatal(err)
   831  	}
   832  	go func() {
   833  		engine.Run()
   834  	}()
   835  
   836  	t.Logf("start change main_test.go")
   837  	// change file of main_test.go
   838  	// just append a new empty line to main_test.go
   839  	if err = waitingPortReady(t, port, time.Second*40); err != nil {
   840  		t.Fatal(err)
   841  	}
   842  	go func() {
   843  		file, err = os.OpenFile("main_test.go", os.O_APPEND|os.O_WRONLY, 0o644)
   844  		assert.NoError(t, err)
   845  		defer file.Close()
   846  		_, err = file.WriteString("\n")
   847  		assert.NoError(t, err)
   848  	}()
   849  	// should Have rebuild
   850  	if err = waitingPortReady(t, port, time.Second*10); err != nil {
   851  		t.Fatal(err)
   852  	}
   853  }
   854  
   855  func TestCreateNewDir(t *testing.T) {
   856  	// generate a random port
   857  	port, f := GetPort()
   858  	f()
   859  	t.Logf("port: %d", port)
   860  
   861  	tmpDir := initTestEnv(t, port)
   862  	// change dir to tmpDir
   863  	chdir(t, tmpDir)
   864  	engine, err := NewEngine("", true)
   865  	if err != nil {
   866  		t.Fatalf("Should not be fail: %s.", err)
   867  	}
   868  
   869  	go func() {
   870  		engine.Run()
   871  	}()
   872  	time.Sleep(time.Second * 2)
   873  	assert.True(t, checkPortHaveBeenUsed(port))
   874  
   875  	// create a new dir make dir
   876  	if err = os.Mkdir(tmpDir+"/dir", 0o644); err != nil {
   877  		t.Fatal(err)
   878  	}
   879  
   880  	// no need reload
   881  	if err = waitingPortConnectionRefused(t, port, 3*time.Second); err == nil {
   882  		t.Fatal("should raise a error")
   883  	}
   884  	engine.Stop()
   885  	time.Sleep(2 * time.Second)
   886  }
   887  
   888  func TestShouldIncludeIncludedFile(t *testing.T) {
   889  	port, f := GetPort()
   890  	f()
   891  	t.Logf("port: %d", port)
   892  
   893  	tmpDir := initTestEnv(t, port)
   894  
   895  	chdir(t, tmpDir)
   896  
   897  	config := `
   898  [build]
   899  cmd = "true" # do nothing
   900  full_bin = "sh main.sh"
   901  include_ext = ["sh"]
   902  include_dir = ["nonexist"] # prevent default "." watch from taking effect
   903  include_file = ["main.sh"]
   904  `
   905  	if err := ioutil.WriteFile(dftTOML, []byte(config), 0o644); err != nil {
   906  		t.Fatal(err)
   907  	}
   908  
   909  	err := os.WriteFile("main.sh", []byte("#!/bin/sh\nprintf original > output"), 0o755)
   910  	if err != nil {
   911  		t.Fatal(err)
   912  	}
   913  
   914  	engine, err := NewEngine(dftTOML, false)
   915  	if err != nil {
   916  		t.Fatal(err)
   917  	}
   918  	go func() {
   919  		engine.Run()
   920  	}()
   921  
   922  	time.Sleep(time.Second * 1)
   923  
   924  	bytes, err := os.ReadFile("output")
   925  	if err != nil {
   926  		t.Fatal(err)
   927  	}
   928  	assert.Equal(t, []byte("original"), bytes)
   929  
   930  	t.Logf("start change main.sh")
   931  	go func() {
   932  		err := os.WriteFile("main.sh", []byte("#!/bin/sh\nprintf modified > output"), 0o755)
   933  		if err != nil {
   934  			log.Fatalf("Error updating file: %s.", err)
   935  		}
   936  	}()
   937  
   938  	time.Sleep(time.Second * 3)
   939  
   940  	bytes, err = os.ReadFile("output")
   941  	if err != nil {
   942  		t.Fatal(err)
   943  	}
   944  	assert.Equal(t, []byte("modified"), bytes)
   945  }