github.com/wtsi-ssg/wrstat@v1.1.4-0.20221008232152-3030622a8cf8/watch/watch_test.go (about)

     1  /*******************************************************************************
     2   * Copyright (c) 2022 Genome Research Ltd.
     3   *
     4   * Author: Sendu Bala <sb10@sanger.ac.uk>
     5   *
     6   * Permission is hereby granted, free of charge, to any person obtaining
     7   * a copy of this software and associated documentation files (the
     8   * "Software"), to deal in the Software without restriction, including
     9   * without limitation the rights to use, copy, modify, merge, publish,
    10   * distribute, sublicense, and/or sell copies of the Software, and to
    11   * permit persons to whom the Software is furnished to do so, subject to
    12   * the following conditions:
    13   *
    14   * The above copyright notice and this permission notice shall be included
    15   * in all copies or substantial portions of the Software.
    16   *
    17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    18   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    19   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    20   * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    21   * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    22   * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    23   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    24   ******************************************************************************/
    25  
    26  package watch
    27  
    28  import (
    29  	"os"
    30  	"path/filepath"
    31  	"sync"
    32  	"testing"
    33  	"time"
    34  
    35  	. "github.com/smartystreets/goconvey/convey"
    36  )
    37  
    38  // testMtimeTracker lets us test mtime updates in our WatcherCallback in a
    39  // thread-safe way.
    40  type testMtimeTracker struct {
    41  	sync.RWMutex
    42  	calls  int
    43  	latest time.Time
    44  }
    45  
    46  // update increments calls and sets latest to the given mtime.
    47  func (m *testMtimeTracker) update(mtime time.Time) {
    48  	m.Lock()
    49  	defer m.Unlock()
    50  
    51  	m.calls++
    52  	m.latest = mtime
    53  }
    54  
    55  // report returns the latest mtime and true if there has both been a call to
    56  // update() since the last call to report(), and the given time is older than
    57  // the latest mtime.
    58  func (m *testMtimeTracker) report(previous time.Time) (time.Time, bool) {
    59  	m.Lock()
    60  	defer m.Unlock()
    61  
    62  	calls := m.calls
    63  	m.calls = 0
    64  
    65  	return m.latest, calls == 1 && previous.Before(m.latest)
    66  }
    67  
    68  // numCalls tells you how many calls to update() there have been since the last
    69  // call to report().
    70  func (m *testMtimeTracker) numCalls() int {
    71  	m.RLock()
    72  	defer m.RUnlock()
    73  
    74  	return m.calls
    75  }
    76  
    77  func TestWatch(t *testing.T) {
    78  	pollFrequency := 10 * time.Millisecond
    79  
    80  	Convey("Given a file to watch", t, func() {
    81  		// lustre can record mtimes earlier than local time, so our 'before'
    82  		// time has to be even more before
    83  		before := time.Now().Add(-1 * time.Second)
    84  
    85  		path := createTestFile(t)
    86  
    87  		Convey("You can create a watcher, which immediately finds the file's mtime", func() {
    88  			tracker := &testMtimeTracker{}
    89  
    90  			cb := func(mtime time.Time) {
    91  				tracker.update(mtime)
    92  			}
    93  
    94  			w, err := New(path, cb, pollFrequency)
    95  			So(err, ShouldBeNil)
    96  			defer w.Stop()
    97  
    98  			calls := tracker.numCalls()
    99  			So(calls, ShouldEqual, 0)
   100  			_, ok := tracker.report(before)
   101  			So(ok, ShouldBeFalse)
   102  
   103  			latest := w.Mtime()
   104  			So(latest.After(before), ShouldBeTrue)
   105  
   106  			Convey("Changing the file's mtime calls cb after some time", func() {
   107  				<-time.After(2 * pollFrequency)
   108  				calls := tracker.numCalls()
   109  				So(calls, ShouldEqual, 0)
   110  				_, ok = tracker.report(latest)
   111  				So(ok, ShouldBeFalse)
   112  
   113  				touchTestFile(path)
   114  				<-time.After(2 * pollFrequency)
   115  
   116  				latest, ok = tracker.report(latest)
   117  				So(ok, ShouldBeTrue)
   118  
   119  				Convey("Stop() ends the polling", func() {
   120  					w.Stop()
   121  					tracker.report(latest)
   122  					calls := tracker.numCalls()
   123  					So(calls, ShouldEqual, 0)
   124  					<-time.After(100 * time.Millisecond)
   125  
   126  					touchTestFile(path)
   127  					<-time.After(2 * pollFrequency)
   128  
   129  					calls = tracker.numCalls()
   130  					So(calls, ShouldEqual, 0)
   131  					_, ok = tracker.report(latest)
   132  					So(ok, ShouldBeFalse)
   133  				})
   134  			})
   135  		})
   136  	})
   137  
   138  	Convey("You can't create a watcher with a bad file", t, func() {
   139  		w, err := New("/foo£@£$%", func(time.Time) {}, pollFrequency)
   140  		So(err, ShouldNotBeNil)
   141  		So(w, ShouldBeNil)
   142  	})
   143  }
   144  
   145  // createTestFile creates a file to test with that will be auto-cleaned up after
   146  // the test. Returns its path.
   147  func createTestFile(t *testing.T) string {
   148  	t.Helper()
   149  
   150  	dir := t.TempDir()
   151  	path := filepath.Join(dir, "file")
   152  	f, err := os.Create(path)
   153  	So(err, ShouldBeNil)
   154  	err = f.Close()
   155  	So(err, ShouldBeNil)
   156  
   157  	return path
   158  }
   159  
   160  // touchTestFile modifies path's a and mtime to the current time.
   161  func touchTestFile(path string) {
   162  	now := time.Now().Local()
   163  	err := os.Chtimes(path, now, now)
   164  	So(err, ShouldBeNil)
   165  }