gitlab.com/xtgo/livefile@v0.0.2/watcher_test.go (about)

     1  package livefile_test
     2  
     3  import (
     4  	"fmt"
     5  	"io/fs"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"gitlab.com/xtgo/livefile"
    13  )
    14  
    15  func TestWatcher(t *testing.T) {
    16  	cwd, err := os.Getwd()
    17  	if err != nil {
    18  		t.Skip("unable to find current directory:", err)
    19  	}
    20  
    21  	t.Cleanup(func() { os.Chdir(cwd) })
    22  
    23  	tests := []struct {
    24  		name        string
    25  		backup      bool
    26  		restore     bool
    27  		callOnWatch bool
    28  		steps       []operation
    29  	}{
    30  		{
    31  			name:        "watch_existing_file",
    32  			callOnWatch: true,
    33  			steps: []operation{
    34  				update("filename", 0600, "filedata"),
    35  				watch("filename", func(string) bool { return true }),
    36  				assertCallbackSuccess("filename"),
    37  				assertRegular("filename", 0600, "filedata"),
    38  			},
    39  		},
    40  		{
    41  			name: "watch_nonexistant_file",
    42  			steps: []operation{
    43  				watch("filename", func(string) bool { return true }),
    44  				update("filename", 0600, "filedata"),
    45  				assertCallbackSuccess("filename"),
    46  				assertRegular("filename", 0600, "filedata"),
    47  			},
    48  		},
    49  		{
    50  			name:        "watch_existing_file_backup",
    51  			backup:      true,
    52  			callOnWatch: true,
    53  			steps: []operation{
    54  				update("filename", 0600, "filedata"),
    55  				watch("filename", func(string) bool { return true }),
    56  				assertCallbackSuccess("filename"),
    57  				assertRegular("filename", 0600, "filedata"),
    58  				assertRegular("filename~", 0600, "filedata"),
    59  			},
    60  		},
    61  		{
    62  			name:        "watch_nonexistant_file_backup",
    63  			backup:      true,
    64  			callOnWatch: true,
    65  			steps: []operation{
    66  				watch("filename", func(string) bool { return true }),
    67  				assertCallbackSuccess("filename"),
    68  				assertMissing("filename"),
    69  				assertMissing("filename~"),
    70  			},
    71  		},
    72  	}
    73  
    74  	for _, tt := range tests {
    75  		t.Run(tt.name, func(t *testing.T) {
    76  			dir := t.TempDir()
    77  
    78  			err := os.Chdir(dir)
    79  			if err != nil {
    80  				t.Skip("unable to change directory:", err)
    81  			}
    82  
    83  			w := livefile.Watcher{
    84  				Backup:      tt.backup,
    85  				Restore:     tt.restore,
    86  				CallOnWatch: tt.callOnWatch,
    87  
    88  				OnError: func(err error) {
    89  					t.Fatal("async error:", err)
    90  				},
    91  			}
    92  
    93  			defer w.Close()
    94  
    95  			tc := testContext{
    96  				watcher: &w,
    97  				events:  make(map[string]chan bool),
    98  			}
    99  
   100  			for _, step := range tt.steps {
   101  				err := step(&tc)
   102  				if err != nil {
   103  					t.Fatal(err)
   104  				}
   105  			}
   106  		})
   107  	}
   108  }
   109  
   110  type testContext struct {
   111  	watcher *livefile.Watcher
   112  	mu      sync.RWMutex
   113  	events  map[string]chan bool
   114  }
   115  
   116  type operation func(tc *testContext) error
   117  
   118  func assertCallbackSuccess(path string) operation {
   119  	return assertCallback(path, true)
   120  }
   121  
   122  func assertCallbackFailure(path string) operation {
   123  	return assertCallback(path, false)
   124  }
   125  
   126  func assertCallback(path string, want bool) operation {
   127  	return func(tc *testContext) error {
   128  		const timeout = 10 * time.Second
   129  
   130  		tc.mu.RLock()
   131  		ch := tc.events[path]
   132  		tc.mu.RUnlock()
   133  
   134  		select {
   135  		case <-time.After(timeout):
   136  			return fmt.Errorf("timeout exceeded waiting for inotify event")
   137  
   138  		case got := <-ch:
   139  			if got == want {
   140  				return nil
   141  			}
   142  
   143  			msg := "callback on %q returned %v, want %v"
   144  
   145  			return fmt.Errorf(msg, path, got, want)
   146  		}
   147  	}
   148  }
   149  
   150  func assertMissing(path string) operation {
   151  	return func(tc *testContext) error {
   152  		_, err := os.Lstat(path)
   153  		if err == nil {
   154  			return os.ErrExist
   155  		}
   156  
   157  		if os.IsNotExist(err) {
   158  			return nil
   159  		}
   160  
   161  		return err
   162  	}
   163  }
   164  
   165  func assertRegular(path string, perm fs.FileMode, data string) operation {
   166  	return func(tc *testContext) error {
   167  		const (
   168  			timeout  = 1500 * time.Millisecond
   169  			interval = 25 * time.Millisecond
   170  		)
   171  
   172  		deadline := time.Now().Add(timeout)
   173  
   174  		var info os.FileInfo
   175  
   176  		for {
   177  			var err error
   178  
   179  			info, err = os.Lstat(path)
   180  			if err == nil {
   181  				break
   182  			}
   183  
   184  			if !os.IsNotExist(err) || time.Now().After(deadline) {
   185  				return err
   186  			}
   187  
   188  			time.Sleep(interval)
   189  		}
   190  
   191  		mode := info.Mode()
   192  		if !mode.IsRegular() {
   193  			return fmt.Errorf("%s is not a regular file", path)
   194  		}
   195  
   196  		mode = mode.Perm()
   197  		if mode != perm {
   198  			return fmt.Errorf("%s has perm %v, want %v", path, mode, perm)
   199  		}
   200  
   201  		buf, err := os.ReadFile(path)
   202  		if err != nil {
   203  			return err
   204  		}
   205  
   206  		if string(buf) != data {
   207  			return fmt.Errorf("%s has content %#q, want %#q", path, buf, data)
   208  		}
   209  
   210  		return nil
   211  	}
   212  }
   213  
   214  func watch(path string, callback livefile.Callback) operation {
   215  	return func(tc *testContext) error {
   216  		tc.mu.Lock()
   217  		defer tc.mu.Unlock()
   218  
   219  		ch := tc.events[path]
   220  		if ch == nil {
   221  			ch = make(chan bool)
   222  			tc.events[path] = ch
   223  		}
   224  
   225  		return tc.watcher.Watch(path,
   226  			func(key string) bool {
   227  				ok := callback(key)
   228  				ch <- ok
   229  
   230  				return ok
   231  			})
   232  	}
   233  }
   234  
   235  func mkdir(path string) operation {
   236  	return func(*testContext) error {
   237  		return os.Mkdir(path, 0700)
   238  	}
   239  }
   240  
   241  func rename(src, dst string) operation {
   242  	return func(*testContext) error {
   243  		return os.Rename(src, dst)
   244  	}
   245  }
   246  
   247  func update(path string, mode fs.FileMode, data string) operation {
   248  	return func(*testContext) error {
   249  		return livefile.Update(path, mode, strings.NewReader(data))
   250  	}
   251  }
   252  
   253  func symlink(src, dst string) operation {
   254  	return func(tc *testContext) error {
   255  		return livefile.Symlink(src, dst)
   256  	}
   257  }
   258  
   259  func unlink(path string) operation {
   260  	return func(tc *testContext) error {
   261  		return livefile.Remove(path)
   262  	}
   263  }