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