github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/watch/watcher_naive_test.go (about)

     1  //go:build !darwin
     2  // +build !darwin
     3  
     4  package watch
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  func TestDontWatchEachFile(t *testing.T) {
    20  	if runtime.GOOS != "linux" {
    21  		t.Skip("This test uses linux-specific inotify checks")
    22  	}
    23  
    24  	// fsnotify is not recursive, so we need to watch each directory
    25  	// you can watch individual files with fsnotify, but that is more prone to exhaust resources
    26  	// this test uses a Linux way to get the number of watches to make sure we're watching
    27  	// per-directory, not per-file
    28  	f := newNotifyFixture(t)
    29  
    30  	watched := f.TempDir("watched")
    31  
    32  	// there are a few different cases we want to test for because the code paths are slightly
    33  	// different:
    34  	// 1) initial: data there before we ever call watch
    35  	// 2) inplace: data we create while the watch is happening
    36  	// 3) staged: data we create in another directory and then atomically move into place
    37  
    38  	// initial
    39  	f.WriteFile(f.JoinPath(watched, "initial.txt"), "initial data")
    40  
    41  	initialDir := f.JoinPath(watched, "initial_dir")
    42  	if err := os.Mkdir(initialDir, 0777); err != nil {
    43  		t.Fatal(err)
    44  	}
    45  
    46  	for i := 0; i < 100; i++ {
    47  		f.WriteFile(f.JoinPath(initialDir, fmt.Sprintf("%d", i)), "initial data")
    48  	}
    49  
    50  	f.watch(watched)
    51  	f.fsync()
    52  	if len(f.events) != 0 {
    53  		t.Fatalf("expected 0 initial events; got %d events: %v", len(f.events), f.events)
    54  	}
    55  	f.events = nil
    56  
    57  	// inplace
    58  	inplace := f.JoinPath(watched, "inplace")
    59  	if err := os.Mkdir(inplace, 0777); err != nil {
    60  		t.Fatal(err)
    61  	}
    62  	f.WriteFile(f.JoinPath(inplace, "inplace.txt"), "inplace data")
    63  
    64  	inplaceDir := f.JoinPath(inplace, "inplace_dir")
    65  	if err := os.Mkdir(inplaceDir, 0777); err != nil {
    66  		t.Fatal(err)
    67  	}
    68  
    69  	for i := 0; i < 100; i++ {
    70  		f.WriteFile(f.JoinPath(inplaceDir, fmt.Sprintf("%d", i)), "inplace data")
    71  	}
    72  
    73  	f.fsync()
    74  	if len(f.events) < 100 {
    75  		t.Fatalf("expected >100 inplace events; got %d events: %v", len(f.events), f.events)
    76  	}
    77  	f.events = nil
    78  
    79  	// staged
    80  	staged := f.TempDir("staged")
    81  	f.WriteFile(f.JoinPath(staged, "staged.txt"), "staged data")
    82  
    83  	stagedDir := f.JoinPath(staged, "staged_dir")
    84  	if err := os.Mkdir(stagedDir, 0777); err != nil {
    85  		t.Fatal(err)
    86  	}
    87  
    88  	for i := 0; i < 100; i++ {
    89  		f.WriteFile(f.JoinPath(stagedDir, fmt.Sprintf("%d", i)), "staged data")
    90  	}
    91  
    92  	if err := os.Rename(staged, f.JoinPath(watched, "staged")); err != nil {
    93  		t.Fatal(err)
    94  	}
    95  
    96  	f.fsync()
    97  	if len(f.events) < 100 {
    98  		t.Fatalf("expected >100 staged events; got %d events: %v", len(f.events), f.events)
    99  	}
   100  	f.events = nil
   101  
   102  	n, err := inotifyNodes()
   103  	require.NoError(t, err)
   104  	if n > 10 {
   105  		t.Fatalf("watching more than 10 files: %d", n)
   106  	}
   107  }
   108  
   109  func inotifyNodes() (int, error) {
   110  	pid := os.Getpid()
   111  
   112  	output, err := exec.Command("bash", "-c", fmt.Sprintf(
   113  		"find /proc/%d/fd -lname anon_inode:inotify -printf '%%hinfo/%%f\n' | xargs cat | grep -c '^inotify'", pid)).Output()
   114  	if err != nil {
   115  		return 0, fmt.Errorf("error running command to determine number of watched files: %v", err)
   116  	}
   117  
   118  	n, err := strconv.Atoi(strings.TrimSpace(string(output)))
   119  	if err != nil {
   120  		return 0, fmt.Errorf("couldn't parse number of watched files: %v", err)
   121  	}
   122  	return n, nil
   123  }
   124  
   125  func TestDontRecurseWhenWatchingParentsOfNonExistentFiles(t *testing.T) {
   126  	if runtime.GOOS != "linux" {
   127  		t.Skip("This test uses linux-specific inotify checks")
   128  	}
   129  
   130  	f := newNotifyFixture(t)
   131  
   132  	watched := f.TempDir("watched")
   133  	f.watch(filepath.Join(watched, ".tiltignore"))
   134  
   135  	excludedDir := f.JoinPath(watched, "excluded")
   136  	for i := 0; i < 10; i++ {
   137  		f.WriteFile(f.JoinPath(excludedDir, fmt.Sprintf("%d", i), "data.txt"), "initial data")
   138  	}
   139  	f.fsync()
   140  
   141  	n, err := inotifyNodes()
   142  	require.NoError(t, err)
   143  	if n > 5 {
   144  		t.Fatalf("watching more than 5 files: %d", n)
   145  	}
   146  }