github.com/brandur/modulir@v0.0.0-20240305213423-94ee82929cbd/watch_test.go (about)

     1  package modulir
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	"github.com/fsnotify/fsnotify"
     8  	assert "github.com/stretchr/testify/require"
     9  )
    10  
    11  func TestBuildWithinSameFileQuiesce(t *testing.T) {
    12  	baseTime := time.Now()
    13  
    14  	lastChanges := map[string]struct{}{"a/path": {}}
    15  	sameLastChanges := map[string]struct{}{"a/path": {}}
    16  	diffLastChanges := map[string]struct{}{"b/path": {}}
    17  
    18  	// No last changes
    19  	assert.False(t, buildWithinSameFileQuiesce(
    20  		baseTime, baseTime, lastChanges, nil,
    21  	))
    22  
    23  	// Rebuild after quiesce time
    24  	assert.False(t, buildWithinSameFileQuiesce(
    25  		baseTime, baseTime.Add(10*time.Second), lastChanges, sameLastChanges,
    26  	))
    27  
    28  	// Different set of canges
    29  	assert.False(t, buildWithinSameFileQuiesce(
    30  		baseTime, baseTime, lastChanges, diffLastChanges,
    31  	))
    32  
    33  	// Within quiesce time and same set of changes
    34  	assert.True(t, buildWithinSameFileQuiesce(
    35  		baseTime, baseTime, lastChanges, sameLastChanges,
    36  	))
    37  }
    38  
    39  func TestCompareKeys(t *testing.T) {
    40  	assert.True(t, compareKeys(
    41  		map[string]struct{}{"a": {}, "b": {}},
    42  		map[string]struct{}{"b": {}, "a": {}},
    43  	))
    44  
    45  	assert.True(t, compareKeys(
    46  		map[string]struct{}{"a": {}, "b": {}, "c": {}},
    47  		map[string]struct{}{"c": {}, "b": {}, "a": {}},
    48  	))
    49  
    50  	assert.False(t, compareKeys(
    51  		map[string]struct{}{"a": {}},
    52  		map[string]struct{}{"b": {}},
    53  	))
    54  
    55  	assert.False(t, compareKeys(
    56  		map[string]struct{}{"a": {}, "b": {}},
    57  		map[string]struct{}{"c": {}, "a": {}},
    58  	))
    59  
    60  	assert.False(t, compareKeys(
    61  		map[string]struct{}{"a": {}},
    62  		nil,
    63  	))
    64  
    65  	assert.False(t, compareKeys(
    66  		nil,
    67  		map[string]struct{}{"a": {}},
    68  	))
    69  }
    70  
    71  func TestShouldRebuild(t *testing.T) {
    72  	// Most things signal a rebuild
    73  	assert.Equal(t, true, shouldRebuild("a/path", fsnotify.Create))
    74  	assert.Equal(t, true, shouldRebuild("a/path", fsnotify.Remove))
    75  	assert.Equal(t, true, shouldRebuild("a/path", fsnotify.Write))
    76  
    77  	// With just a few special cases that don't
    78  	assert.Equal(t, false, shouldRebuild("a/path", fsnotify.Chmod))
    79  	assert.Equal(t, false, shouldRebuild("a/path", fsnotify.Rename))
    80  	assert.Equal(t, false, shouldRebuild("a/.DS_Store", fsnotify.Create))
    81  	assert.Equal(t, false, shouldRebuild("a/4913", fsnotify.Create))
    82  	assert.Equal(t, false, shouldRebuild("a/path~", fsnotify.Create))
    83  }
    84  
    85  func TestWatchChanges(t *testing.T) {
    86  	watchEvents := make(chan fsnotify.Event, 1)
    87  	watchErrors := make(chan error, 1)
    88  	rebuild := make(chan map[string]struct{}, 1)
    89  	rebuildDone := make(chan struct{}, 1)
    90  
    91  	go watchChanges(newContext(), watchEvents, watchErrors,
    92  		rebuild, rebuildDone)
    93  
    94  	{
    95  		// An ineligible even that will be ignored.
    96  		watchEvents <- fsnotify.Event{Name: "a/path~", Op: fsnotify.Create}
    97  
    98  		select {
    99  		case <-rebuild:
   100  			assert.Fail(t, "Should not have received rebuild on ineligible event")
   101  		case <-time.After(50 * time.Millisecond):
   102  		}
   103  	}
   104  
   105  	{
   106  		// An valid event.
   107  		watchEvents <- fsnotify.Event{Name: "a/path", Op: fsnotify.Create}
   108  
   109  		select {
   110  		case sources := <-rebuild:
   111  			assert.Equal(t, map[string]struct{}{"a/path": {}}, sources)
   112  		case <-time.After(50 * time.Millisecond):
   113  			assert.Fail(t, "Should have received a rebuild signal")
   114  		}
   115  
   116  		// While we're rebuilding, the watcher will accumulate events. Send a
   117  		// few more that are eligible and one that's not.
   118  		watchEvents <- fsnotify.Event{Name: "a/path1", Op: fsnotify.Create}
   119  		watchEvents <- fsnotify.Event{Name: "a/path2", Op: fsnotify.Create}
   120  		watchEvents <- fsnotify.Event{Name: "a/path~", Op: fsnotify.Create}
   121  
   122  		// Signal that the build is finished
   123  		rebuildDone <- struct{}{}
   124  
   125  		// Now verify that we got the accumulated changes.
   126  		select {
   127  		case sources := <-rebuild:
   128  			assert.Equal(t, map[string]struct{}{
   129  				"a/path1": {},
   130  				"a/path2": {},
   131  			}, sources)
   132  		case <-time.After(50 * time.Millisecond):
   133  			assert.Fail(t, "Should have received a rebuild signal")
   134  		}
   135  
   136  		// Send one more rebuild done so the watcher can continue
   137  		rebuildDone <- struct{}{}
   138  	}
   139  
   140  	// Finish up by closing the channel to stop the loop
   141  	close(watchEvents)
   142  }
   143  
   144  // Helper to easily create a new Modulir context.
   145  func newContext() *Context {
   146  	return NewContext(&Args{Log: &Logger{Level: LevelInfo}})
   147  }