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  }