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  }