github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/lockedfile/internal/filelock/filelock_test.go (about)

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