github.com/blend/go-sdk@v1.20220411.3/filelock/filelock_test.go (about)

     1  //go:build !js && !plan9
     2  // +build !js,!plan9
     3  
     4  /*
     5  
     6  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     7  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     8  
     9  */
    10  
    11  package filelock_test
    12  
    13  import (
    14  	"fmt"
    15  	"os"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"runtime"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/blend/go-sdk/filelock"
    23  )
    24  
    25  func lock(t *testing.T, f *os.File) {
    26  	t.Helper()
    27  
    28  	err := filelock.Lock(f)
    29  	t.Logf("Lock(fd %d) = %v", f.Fd(), err)
    30  	if err != nil {
    31  		t.Fail()
    32  	}
    33  }
    34  
    35  func rLock(t *testing.T, f *os.File) {
    36  	t.Helper()
    37  
    38  	err := filelock.RLock(f)
    39  	t.Logf("RLock(fd %d) = %v", f.Fd(), err)
    40  	if err != nil {
    41  		t.Fail()
    42  	}
    43  }
    44  
    45  func unlock(t *testing.T, f *os.File) {
    46  	t.Helper()
    47  
    48  	err := filelock.Unlock(f)
    49  	t.Logf("Unlock(fd %d) = %v", f.Fd(), err)
    50  	if err != nil {
    51  		t.Fail()
    52  	}
    53  }
    54  
    55  func mustTempFile(t *testing.T) (f *os.File, remove func()) {
    56  	t.Helper()
    57  
    58  	base := filepath.Base(t.Name())
    59  	f, err := os.CreateTemp("", base)
    60  	if err != nil {
    61  		t.Fatalf(`os.CreateTemp("", %q) = %v`, base, err)
    62  	}
    63  	t.Logf("fd %d = %s", f.Fd(), f.Name())
    64  
    65  	return f, func() {
    66  		f.Close()
    67  		os.Remove(f.Name())
    68  	}
    69  }
    70  
    71  func mustOpen(t *testing.T, name string) *os.File {
    72  	t.Helper()
    73  
    74  	f, err := os.OpenFile(name, os.O_RDWR, 0)
    75  	if err != nil {
    76  		t.Fatalf("os.Open(%q) = %v", name, err)
    77  	}
    78  
    79  	t.Logf("fd %d = os.Open(%q)", f.Fd(), name)
    80  	return f
    81  }
    82  
    83  const (
    84  	quiescent            = 10 * time.Millisecond
    85  	probablyStillBlocked = 10 * time.Second
    86  )
    87  
    88  func mustBlock(t *testing.T, op string, f *os.File) (wait func(*testing.T)) {
    89  	t.Helper()
    90  
    91  	desc := fmt.Sprintf("%s(fd %d)", op, f.Fd())
    92  
    93  	done := make(chan struct{})
    94  	go func() {
    95  		t.Helper()
    96  		switch op {
    97  		case "Lock":
    98  			lock(t, f)
    99  		case "RLock":
   100  			rLock(t, f)
   101  		default:
   102  			panic("invalid op: " + op)
   103  		}
   104  		close(done)
   105  	}()
   106  
   107  	select {
   108  	case <-done:
   109  		t.Fatalf("%s unexpectedly did not block", desc)
   110  		return nil
   111  
   112  	case <-time.After(quiescent):
   113  		t.Logf("%s is blocked (as expected)", desc)
   114  		return func(t *testing.T) {
   115  			t.Helper()
   116  			select {
   117  			case <-time.After(probablyStillBlocked):
   118  				t.Fatalf("%s is unexpectedly still blocked", desc)
   119  			case <-done:
   120  			}
   121  		}
   122  	}
   123  }
   124  
   125  func TestLockExcludesLock(t *testing.T) {
   126  	t.Parallel()
   127  
   128  	f, remove := mustTempFile(t)
   129  	defer remove()
   130  
   131  	other := mustOpen(t, f.Name())
   132  	defer other.Close()
   133  
   134  	lock(t, f)
   135  	lockOther := mustBlock(t, "Lock", other)
   136  	unlock(t, f)
   137  	lockOther(t)
   138  	unlock(t, other)
   139  }
   140  
   141  func TestLockExcludesRLock(t *testing.T) {
   142  	t.Parallel()
   143  
   144  	f, remove := mustTempFile(t)
   145  	defer remove()
   146  
   147  	other := mustOpen(t, f.Name())
   148  	defer other.Close()
   149  
   150  	lock(t, f)
   151  	rLockOther := mustBlock(t, "RLock", other)
   152  	unlock(t, f)
   153  	rLockOther(t)
   154  	unlock(t, other)
   155  }
   156  
   157  func TestRLockExcludesOnlyLock(t *testing.T) {
   158  	t.Parallel()
   159  
   160  	f, remove := mustTempFile(t)
   161  	defer remove()
   162  	rLock(t, f)
   163  
   164  	f2 := mustOpen(t, f.Name())
   165  	defer f2.Close()
   166  
   167  	doUnlockTF := false
   168  	switch runtime.GOOS {
   169  	case "aix", "solaris":
   170  		// When using POSIX locks (as on Solaris), we can't safely read-lock the
   171  		// same inode through two different descriptors at the same time: when the
   172  		// first descriptor is closed, the second descriptor would still be open but
   173  		// silently unlocked. So a second RLock must block instead of proceeding.
   174  		lockF2 := mustBlock(t, "RLock", f2)
   175  		unlock(t, f)
   176  		lockF2(t)
   177  	default:
   178  		rLock(t, f2)
   179  		doUnlockTF = true
   180  	}
   181  
   182  	other := mustOpen(t, f.Name())
   183  	defer other.Close()
   184  	lockOther := mustBlock(t, "Lock", other)
   185  
   186  	unlock(t, f2)
   187  	if doUnlockTF {
   188  		unlock(t, f)
   189  	}
   190  	lockOther(t)
   191  	unlock(t, other)
   192  }
   193  
   194  func TestLockNotDroppedByExecCommand(t *testing.T) {
   195  	MustHaveExec(t)
   196  
   197  	f, remove := mustTempFile(t)
   198  	defer remove()
   199  
   200  	lock(t, f)
   201  
   202  	other := mustOpen(t, f.Name())
   203  	defer other.Close()
   204  
   205  	// Some kinds of file locks are dropped when a duplicated or forked file
   206  	// descriptor is unlocked. Double-check that the approach used by os/exec does
   207  	// not accidentally drop locks.
   208  	cmd := exec.Command(os.Args[0], "-test.run=^$")
   209  	if err := cmd.Run(); err != nil {
   210  		t.Fatalf("exec failed: %v", err)
   211  	}
   212  
   213  	lockOther := mustBlock(t, "Lock", other)
   214  	unlock(t, f)
   215  	lockOther(t)
   216  	unlock(t, other)
   217  }