github.com/cortesi/moddwatch@v0.1.0/watch_test.go (about)

     1  package moddwatch
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/url"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"reflect"
    11  	"runtime"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/google/go-cmp/cmp"
    17  	"github.com/rjeczalik/notify"
    18  )
    19  
    20  var alwaysEqual = cmp.Comparer(func(_, _ interface{}) bool { return true })
    21  var cmpOptions = cmp.Options{
    22  	cmp.FilterValues(
    23  		func(x, y interface{}) bool {
    24  			vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
    25  			return (vx.IsValid() && vy.IsValid() &&
    26  				vx.Type() == vy.Type()) &&
    27  				(vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) &&
    28  				(vx.Len() == 0 && vy.Len() == 0)
    29  		},
    30  		alwaysEqual,
    31  	),
    32  	cmp.FilterPath(
    33  		func(p cmp.Path) bool {
    34  			if p.String() == "URL.RawQuery" {
    35  				return true
    36  			}
    37  			return false
    38  		},
    39  		cmp.Comparer(func(a, b interface{}) bool {
    40  			qa, _ := url.ParseQuery(a.(string))
    41  			qb, _ := url.ParseQuery(b.(string))
    42  			return cmp.Equal(qa, qb)
    43  		}),
    44  	),
    45  }
    46  
    47  // WithTempDir creates a temp directory, changes the current working directory
    48  // to it, and returns a function that can be called to clean up. Use it like
    49  // this:
    50  //      defer WithTempDir(t)()
    51  func WithTempDir(t *testing.T) func() {
    52  	cwd, err := os.Getwd()
    53  	if err != nil {
    54  		t.Fatalf("TempDir: %v", err)
    55  	}
    56  	tmpdir, err := ioutil.TempDir("", "")
    57  	if err != nil {
    58  		t.Fatalf("TempDir: %v", err)
    59  	}
    60  	err = os.Chdir(tmpdir)
    61  	if err != nil {
    62  		t.Fatalf("Chdir: %v", err)
    63  	}
    64  	return func() {
    65  		err := os.Chdir(cwd)
    66  		if err != nil {
    67  			t.Fatalf("Chdir: %v", err)
    68  		}
    69  		err = os.RemoveAll(tmpdir)
    70  		if err != nil {
    71  			t.Fatalf("Removing tmpdir: %s", err)
    72  		}
    73  	}
    74  }
    75  
    76  type TEventInfo struct {
    77  	event notify.Event
    78  	path  string
    79  }
    80  
    81  func (te TEventInfo) Path() string {
    82  	return te.path
    83  }
    84  
    85  func (te TEventInfo) Event() notify.Event {
    86  	return te.event
    87  }
    88  
    89  func (te TEventInfo) Sys() interface{} {
    90  	return nil
    91  }
    92  
    93  type testExistenceChecker struct {
    94  	paths map[string]bool
    95  }
    96  
    97  func (e *testExistenceChecker) Check(p string) bool {
    98  	_, ok := e.paths[p]
    99  	return ok
   100  }
   101  
   102  func exists(paths ...string) *testExistenceChecker {
   103  	et := testExistenceChecker{make(map[string]bool)}
   104  	for _, p := range paths {
   105  		et.paths[p] = true
   106  	}
   107  	return &et
   108  }
   109  
   110  var batchTests = []struct {
   111  	events   []TEventInfo
   112  	exists   *testExistenceChecker
   113  	expected Mod
   114  }{
   115  	{
   116  		[]TEventInfo{
   117  			TEventInfo{notify.Create, "foo"},
   118  			TEventInfo{notify.Create, "bar"},
   119  		},
   120  		exists("bar", "foo"),
   121  		Mod{Added: []string{"bar", "foo"}},
   122  	},
   123  	{
   124  		[]TEventInfo{
   125  			TEventInfo{notify.Rename, "foo"},
   126  			TEventInfo{notify.Rename, "bar"},
   127  		},
   128  		exists("foo"),
   129  		Mod{Added: []string{"foo"}, Deleted: []string{"bar"}},
   130  	},
   131  	{
   132  		[]TEventInfo{
   133  			TEventInfo{notify.Write, "foo"},
   134  		},
   135  		exists("foo"),
   136  		Mod{Changed: []string{"foo"}},
   137  	},
   138  	{
   139  		[]TEventInfo{
   140  			TEventInfo{notify.Write, "foo"},
   141  			TEventInfo{notify.Remove, "foo"},
   142  		},
   143  		exists(),
   144  		Mod{Deleted: []string{"foo"}},
   145  	},
   146  	{
   147  		[]TEventInfo{
   148  			TEventInfo{notify.Remove, "foo"},
   149  		},
   150  		exists("foo"),
   151  		Mod{},
   152  	},
   153  	{
   154  		[]TEventInfo{
   155  			TEventInfo{notify.Create, "foo"},
   156  			TEventInfo{notify.Create, "bar"},
   157  			TEventInfo{notify.Remove, "bar"},
   158  		},
   159  		exists("bar", "foo"),
   160  		Mod{Added: []string{"bar", "foo"}},
   161  	},
   162  	{
   163  		[]TEventInfo{
   164  			TEventInfo{notify.Create, "foo"},
   165  		},
   166  		exists(),
   167  		Mod{},
   168  	},
   169  }
   170  
   171  func TestBatch(t *testing.T) {
   172  	for i, tst := range batchTests {
   173  		input := make(chan notify.EventInfo, len(tst.events))
   174  		for _, e := range tst.events {
   175  			input <- e
   176  		}
   177  		ret := batch(time.Millisecond*10, MaxLullWait, tst.exists, input)
   178  		if !reflect.DeepEqual(*ret, tst.expected) {
   179  			t.Errorf("Test %d: expected\n%#v\ngot\n%#v", i, tst.expected, ret)
   180  		}
   181  	}
   182  }
   183  
   184  func abs(path string) string {
   185  	wd, err := os.Getwd()
   186  	if err != nil {
   187  		panic("Could not get current working directory")
   188  	}
   189  	return filepath.ToSlash(filepath.Join(wd, path))
   190  }
   191  
   192  var isUnderTests = []struct {
   193  	parent   string
   194  	child    string
   195  	expected bool
   196  }{
   197  	{"/foo", "/foo/bar", true},
   198  	{"/foo", "/foo", true},
   199  	{"/foo", "/foobar/bar", false},
   200  }
   201  
   202  func TestIsUnder(t *testing.T) {
   203  	for i, tst := range isUnderTests {
   204  		ret := isUnder(tst.parent, tst.child)
   205  		if ret != tst.expected {
   206  			t.Errorf("Test %d: expected %#v, got %#v", i, tst.expected, ret)
   207  		}
   208  	}
   209  }
   210  
   211  func TestMod(t *testing.T) {
   212  	if !(Mod{}.Empty()) {
   213  		t.Error("Expected mod to be empty.")
   214  	}
   215  	m := Mod{
   216  		Added:   []string{"add"},
   217  		Deleted: []string{"rm"},
   218  		Changed: []string{"change"},
   219  	}
   220  	if m.Empty() {
   221  		t.Error("Expected mod not to be empty")
   222  	}
   223  	if !reflect.DeepEqual(m.All(), []string{"add", "change"}) {
   224  		t.Error("Unexpeced return from Mod.All")
   225  	}
   226  
   227  	m = Mod{
   228  		Added:   []string{abs("add")},
   229  		Deleted: []string{abs("rm")},
   230  		Changed: []string{abs("change")},
   231  	}
   232  	if _, err := m.normPaths("."); err != nil {
   233  		t.Error(err)
   234  	}
   235  }
   236  
   237  func testListBasic(t *testing.T) {
   238  	var findTests = []struct {
   239  		include  []string
   240  		exclude  []string
   241  		expected []string
   242  	}{
   243  		{
   244  			[]string{"**"},
   245  			[]string{},
   246  			[]string{"a/a.test1", "a/b.test2", "b/a.test1", "b/b.test2", "x", "x.test1"},
   247  		},
   248  		{
   249  			[]string{"**/*.test1"},
   250  			[]string{},
   251  			[]string{"a/a.test1", "b/a.test1", "x.test1"},
   252  		},
   253  		{
   254  			[]string{"a"},
   255  			[]string{},
   256  			[]string{},
   257  		},
   258  		{
   259  			[]string{"x"},
   260  			[]string{},
   261  			[]string{"x"},
   262  		},
   263  		{
   264  			[]string{"a/a.test1"},
   265  			[]string{},
   266  			[]string{"a/a.test1"},
   267  		},
   268  		{
   269  			[]string{"**"},
   270  			[]string{"*.test1"},
   271  			[]string{"a/a.test1", "a/b.test2", "b/a.test1", "b/b.test2", "x"},
   272  		},
   273  		{
   274  			[]string{"**"},
   275  			[]string{"a/**"},
   276  			[]string{"b/a.test1", "b/b.test2", "x", "x.test1"},
   277  		},
   278  		{
   279  			[]string{"**"},
   280  			[]string{"a/*"},
   281  			[]string{"b/a.test1", "b/b.test2", "x", "x.test1"},
   282  		},
   283  		{
   284  			[]string{"**"},
   285  			[]string{"**/*.test1", "**/*.test2"},
   286  			[]string{"x"},
   287  		},
   288  	}
   289  
   290  	defer WithTempDir(t)()
   291  	paths := []string{
   292  		"a/a.test1",
   293  		"a/b.test2",
   294  		"b/a.test1",
   295  		"b/b.test2",
   296  		"x",
   297  		"x.test1",
   298  	}
   299  	for _, p := range paths {
   300  		dst := filepath.Join(".", p)
   301  		err := os.MkdirAll(filepath.Dir(dst), 0777)
   302  		if err != nil {
   303  			t.Fatalf("Error creating test dir: %v", err)
   304  		}
   305  		err = ioutil.WriteFile(dst, []byte("test"), 0777)
   306  		if err != nil {
   307  			t.Fatalf("Error writing test file: %v", err)
   308  		}
   309  	}
   310  
   311  	for i, tt := range findTests {
   312  		ret, err := List(".", tt.include, tt.exclude)
   313  		if err != nil {
   314  			t.Fatal(err)
   315  		}
   316  		expected := tt.expected
   317  		for i := range ret {
   318  			ret[i] = filepath.ToSlash(ret[i])
   319  		}
   320  		if !reflect.DeepEqual(ret, expected) {
   321  			t.Errorf(
   322  				"%d: %#v, %#v - Expected\n%#v\ngot:\n%#v",
   323  				i, tt.include, tt.exclude, expected, ret,
   324  			)
   325  		}
   326  	}
   327  }
   328  
   329  func testList(t *testing.T) {
   330  	var findTests = []struct {
   331  		include  []string
   332  		exclude  []string
   333  		expected []string
   334  	}{
   335  		{
   336  			[]string{"**"},
   337  			[]string{},
   338  			[]string{"a/a.test1", "a/b.test2", "a/sub/c.test2", "b/a.test1", "b/b.test2", "x", "x.test1"},
   339  		},
   340  		{
   341  			[]string{"**/*.test1"},
   342  			[]string{},
   343  			[]string{"a/a.test1", "b/a.test1", "x.test1"},
   344  		},
   345  		{
   346  			[]string{"**"},
   347  			[]string{"*.test1"},
   348  			[]string{"a/a.test1", "a/b.test2", "a/sub/c.test2", "b/a.test1", "b/b.test2", "x"},
   349  		},
   350  		{
   351  			[]string{"**"},
   352  			[]string{"a/**"},
   353  			[]string{"b/a.test1", "b/b.test2", "x", "x.test1"},
   354  		},
   355  		{
   356  			[]string{"**"},
   357  			[]string{"a/**"},
   358  			[]string{"b/a.test1", "b/b.test2", "x", "x.test1"},
   359  		},
   360  		{
   361  			[]string{"**"},
   362  			[]string{"**/*.test1", "**/*.test2"},
   363  			[]string{"x"},
   364  		},
   365  		{
   366  			[]string{"a/relsymlink"},
   367  			[]string{},
   368  			[]string{},
   369  		},
   370  		{
   371  			[]string{"a/relfilesymlink"},
   372  			[]string{},
   373  			[]string{"x"},
   374  		},
   375  		{
   376  			[]string{"a/relsymlink/**"},
   377  			[]string{},
   378  			[]string{"b/a.test1", "b/b.test2"},
   379  		},
   380  		{
   381  			[]string{"a/**", "a/relsymlink/**"},
   382  			[]string{},
   383  			[]string{"a/a.test1", "a/b.test2", "a/sub/c.test2", "b/a.test1", "b/b.test2"},
   384  		},
   385  		{
   386  			[]string{"a/abssymlink/**"},
   387  			[]string{},
   388  			[]string{"b/a.test1", "b/b.test2"},
   389  		},
   390  		{
   391  			[]string{"a/**", "a/abssymlink/**"},
   392  			[]string{},
   393  			[]string{"a/a.test1", "a/b.test2", "a/sub/c.test2", "b/a.test1", "b/b.test2"},
   394  		},
   395  	}
   396  
   397  	defer WithTempDir(t)()
   398  	paths := []string{
   399  		"a/a.test1",
   400  		"a/b.test2",
   401  		"a/sub/c.test2",
   402  		"b/a.test1",
   403  		"b/b.test2",
   404  		"x",
   405  		"x.test1",
   406  	}
   407  	for _, p := range paths {
   408  		dst := path.Join(".", p)
   409  		err := os.MkdirAll(path.Dir(dst), 0777)
   410  		if err != nil {
   411  			t.Fatalf("Error creating test dir: %v", err)
   412  		}
   413  		err = ioutil.WriteFile(dst, []byte("test"), 0777)
   414  		if err != nil {
   415  			t.Fatalf("Error writing test file: %v", err)
   416  		}
   417  	}
   418  	if err := os.Symlink("../../b", "./a/relsymlink"); err != nil {
   419  		t.Fatal(err)
   420  		return
   421  	}
   422  	if err := os.Symlink("../../x", "./a/relfilesymlink"); err != nil {
   423  		t.Fatal(err)
   424  		return
   425  	}
   426  
   427  	sabs, err := filepath.Abs(filepath.FromSlash("./b"))
   428  	if err != nil {
   429  		t.Fatal(err)
   430  		return
   431  	}
   432  	if err = os.Symlink(sabs, "./a/abssymlink"); err != nil {
   433  		t.Fatal(err)
   434  		return
   435  	}
   436  
   437  	for i, tt := range findTests {
   438  		t.Run(
   439  			fmt.Sprintf("%.3d", i),
   440  			func(t *testing.T) {
   441  				ret, err := List(".", tt.include, tt.exclude)
   442  				if err != nil {
   443  					t.Fatal(err)
   444  				}
   445  				expected := tt.expected
   446  				for i := range ret {
   447  					if filepath.IsAbs(ret[i]) {
   448  						wd, err := os.Getwd()
   449  						rel, err := filepath.Rel(wd, filepath.ToSlash(ret[i]))
   450  						if err != nil {
   451  							t.Fatal(err)
   452  							return
   453  						}
   454  						ret[i] = rel
   455  					} else {
   456  						ret[i] = filepath.ToSlash(ret[i])
   457  					}
   458  				}
   459  				if !reflect.DeepEqual(ret, expected) {
   460  					t.Errorf(
   461  						"%d: %#v, %#v - Expected\n%#v\ngot:\n%#v",
   462  						i, tt.include, tt.exclude, expected, ret,
   463  					)
   464  				}
   465  			},
   466  		)
   467  	}
   468  }
   469  
   470  func TestList(t *testing.T) {
   471  	testListBasic(t)
   472  	if runtime.GOOS != "windows" {
   473  		testList(t)
   474  	}
   475  }
   476  
   477  const timeout = 2 * time.Second
   478  
   479  func wait(p string) {
   480  	p = filepath.FromSlash(p)
   481  	for {
   482  		_, err := os.Stat(p)
   483  		if err != nil {
   484  			continue
   485  		} else {
   486  			break
   487  		}
   488  	}
   489  }
   490  
   491  func touch(p string) {
   492  	p = filepath.FromSlash(p)
   493  	d := filepath.Dir(p)
   494  	err := os.MkdirAll(d, 0777)
   495  	if err != nil {
   496  		panic(err)
   497  	}
   498  
   499  	f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0777)
   500  	if err != nil {
   501  		panic(err)
   502  	}
   503  	if _, err := f.Write([]byte("teststring")); err != nil {
   504  		panic(err)
   505  	}
   506  	if err := f.Close(); err != nil {
   507  		panic(err)
   508  	}
   509  	ioutil.ReadFile(p)
   510  }
   511  
   512  func events(p string) []string {
   513  	parts := []string{}
   514  	for _, p := range strings.Split(p, "\n") {
   515  		if strings.HasPrefix(p, ":") {
   516  			p = strings.TrimSpace(p)
   517  			if !strings.HasSuffix(p, ":") {
   518  				parts = append(parts, strings.TrimSpace(p))
   519  			}
   520  		}
   521  	}
   522  	return parts
   523  }
   524  
   525  func _testWatch(
   526  	t *testing.T,
   527  	modfunc func(),
   528  	includes []string,
   529  	excludes []string,
   530  	expected Mod,
   531  ) {
   532  	defer WithTempDir(t)()
   533  
   534  	err := os.MkdirAll("a", 0777)
   535  	if err != nil {
   536  		t.Fatal(err)
   537  	}
   538  
   539  	err = os.MkdirAll("b", 0777)
   540  	if err != nil {
   541  		t.Fatal(err)
   542  	}
   543  
   544  	ch := make(chan *Mod, 1024)
   545  	cwd, err := os.Getwd()
   546  	if err != nil {
   547  		t.Fatal(err)
   548  		return
   549  	}
   550  	watcher, err := Watch(
   551  		cwd,
   552  		includes,
   553  		excludes,
   554  		time.Millisecond*200,
   555  		ch,
   556  	)
   557  	if err != nil {
   558  		t.Fatal(err)
   559  		return
   560  	}
   561  	defer watcher.Stop()
   562  	go func() {
   563  		time.Sleep(2 * time.Second)
   564  		watcher.Stop()
   565  	}()
   566  
   567  	// There's some race condition in rjeczalik/notify. If we don't wait a bit
   568  	// here, we sometimes don't receive notifications for the initial event.
   569  	go func() {
   570  		touch("a/initial")
   571  	}()
   572  	for {
   573  		evt, more := <-ch
   574  		if !more {
   575  			t.Errorf("Never saw initial sync event")
   576  			return
   577  		}
   578  		if cmp.Equal(evt.Added, []string{"a/initial"}) {
   579  			break
   580  		} else {
   581  			t.Errorf("Unexpected initial sync event:\n%#v", evt)
   582  			return
   583  		}
   584  	}
   585  
   586  	go modfunc()
   587  	ret := Mod{}
   588  	for {
   589  		evt, more := <-ch
   590  		if more {
   591  			ret = ret.Join(*evt)
   592  			if cmp.Equal(ret, expected, cmpOptions) {
   593  				watcher.Stop()
   594  				return
   595  			}
   596  		} else {
   597  			break
   598  		}
   599  	}
   600  	t.Errorf("Never saw expected result, did see\n%s", ret)
   601  }
   602  
   603  func TestWatch(t *testing.T) {
   604  	t.Run(
   605  		"simple",
   606  		func(t *testing.T) {
   607  			_testWatch(
   608  				t,
   609  				func() {
   610  					touch("a/touched")
   611  					touch("a/initial")
   612  				},
   613  				[]string{"**"},
   614  				[]string{},
   615  				Mod{
   616  					Added:   []string{"a/touched"},
   617  					Changed: []string{"a/initial"},
   618  				},
   619  			)
   620  		},
   621  	)
   622  	t.Run(
   623  		"direct",
   624  		func(t *testing.T) {
   625  			_testWatch(
   626  				t,
   627  				func() {
   628  					touch("a/direct")
   629  				},
   630  				[]string{"a/initial", "a/direct"},
   631  				[]string{},
   632  				Mod{
   633  					Added: []string{"a/direct"},
   634  				},
   635  			)
   636  		},
   637  	)
   638  	t.Run(
   639  		"directprexisting",
   640  		func(t *testing.T) {
   641  			_testWatch(
   642  				t,
   643  				func() {
   644  					touch("a/initial")
   645  				},
   646  				[]string{"a/initial"},
   647  				[]string{},
   648  				Mod{
   649  					Changed: []string{"a/initial"},
   650  				},
   651  			)
   652  		},
   653  	)
   654  	t.Run(
   655  		"deepdirect",
   656  		func(t *testing.T) {
   657  			// On Linux, We can't currently pick up changes within directories
   658  			// created after the watch started. See here for more:
   659  			//
   660  			// https://github.com/cortesi/modd/issues/44
   661  			if runtime.GOOS != "linux" {
   662  				_testWatch(
   663  					t,
   664  					func() {
   665  						touch("a/deep/directory/direct")
   666  					},
   667  					[]string{"a/initial", "a/deep/directory/direct"},
   668  					[]string{},
   669  					Mod{
   670  						Added: []string{"a/deep/directory/direct"},
   671  					},
   672  				)
   673  			}
   674  		},
   675  	)
   676  }