github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/lockedfile/lockedfile_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  // js and wasip1 do not support inter-process file locking.
     6  //
     7  //go:build !js && !wasip1
     8  
     9  package lockedfile_test
    10  
    11  import (
    12  	"fmt"
    13  	"os"
    14  	"path/filepath"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/go-asm/go/testenv"
    19  
    20  	"github.com/go-asm/go/cmd/go/lockedfile"
    21  )
    22  
    23  func mustTempDir(t *testing.T) (dir string, remove func()) {
    24  	t.Helper()
    25  
    26  	dir, err := os.MkdirTemp("", filepath.Base(t.Name()))
    27  	if err != nil {
    28  		t.Fatal(err)
    29  	}
    30  	return dir, func() { os.RemoveAll(dir) }
    31  }
    32  
    33  const (
    34  	quiescent            = 10 * time.Millisecond
    35  	probablyStillBlocked = 10 * time.Second
    36  )
    37  
    38  func mustBlock(t *testing.T, desc string, f func()) (wait func(*testing.T)) {
    39  	t.Helper()
    40  
    41  	done := make(chan struct{})
    42  	go func() {
    43  		f()
    44  		close(done)
    45  	}()
    46  
    47  	timer := time.NewTimer(quiescent)
    48  	defer timer.Stop()
    49  	select {
    50  	case <-done:
    51  		t.Fatalf("%s unexpectedly did not block", desc)
    52  	case <-timer.C:
    53  	}
    54  
    55  	return func(t *testing.T) {
    56  		logTimer := time.NewTimer(quiescent)
    57  		defer logTimer.Stop()
    58  
    59  		select {
    60  		case <-logTimer.C:
    61  			// We expect the operation to have unblocked by now,
    62  			// but maybe it's just slow. Write to the test log
    63  			// in case the test times out, but don't fail it.
    64  			t.Helper()
    65  			t.Logf("%s is unexpectedly still blocked after %v", desc, quiescent)
    66  
    67  			// Wait for the operation to actually complete, no matter how long it
    68  			// takes. If the test has deadlocked, this will cause the test to time out
    69  			// and dump goroutines.
    70  			<-done
    71  
    72  		case <-done:
    73  		}
    74  	}
    75  }
    76  
    77  func TestMutexExcludes(t *testing.T) {
    78  	t.Parallel()
    79  
    80  	dir, remove := mustTempDir(t)
    81  	defer remove()
    82  
    83  	path := filepath.Join(dir, "lock")
    84  
    85  	mu := lockedfile.MutexAt(path)
    86  	t.Logf("mu := MutexAt(_)")
    87  
    88  	unlock, err := mu.Lock()
    89  	if err != nil {
    90  		t.Fatalf("mu.Lock: %v", err)
    91  	}
    92  	t.Logf("unlock, _  := mu.Lock()")
    93  
    94  	mu2 := lockedfile.MutexAt(mu.Path)
    95  	t.Logf("mu2 := MutexAt(mu.Path)")
    96  
    97  	wait := mustBlock(t, "mu2.Lock()", func() {
    98  		unlock2, err := mu2.Lock()
    99  		if err != nil {
   100  			t.Errorf("mu2.Lock: %v", err)
   101  			return
   102  		}
   103  		t.Logf("unlock2, _ := mu2.Lock()")
   104  		t.Logf("unlock2()")
   105  		unlock2()
   106  	})
   107  
   108  	t.Logf("unlock()")
   109  	unlock()
   110  	wait(t)
   111  }
   112  
   113  func TestReadWaitsForLock(t *testing.T) {
   114  	t.Parallel()
   115  
   116  	dir, remove := mustTempDir(t)
   117  	defer remove()
   118  
   119  	path := filepath.Join(dir, "timestamp.txt")
   120  
   121  	f, err := lockedfile.Create(path)
   122  	if err != nil {
   123  		t.Fatalf("Create: %v", err)
   124  	}
   125  	defer f.Close()
   126  
   127  	const (
   128  		part1 = "part 1\n"
   129  		part2 = "part 2\n"
   130  	)
   131  	_, err = f.WriteString(part1)
   132  	if err != nil {
   133  		t.Fatalf("WriteString: %v", err)
   134  	}
   135  	t.Logf("WriteString(%q) = <nil>", part1)
   136  
   137  	wait := mustBlock(t, "Read", func() {
   138  		b, err := lockedfile.Read(path)
   139  		if err != nil {
   140  			t.Errorf("Read: %v", err)
   141  			return
   142  		}
   143  
   144  		const want = part1 + part2
   145  		got := string(b)
   146  		if got == want {
   147  			t.Logf("Read(_) = %q", got)
   148  		} else {
   149  			t.Errorf("Read(_) = %q, _; want %q", got, want)
   150  		}
   151  	})
   152  
   153  	_, err = f.WriteString(part2)
   154  	if err != nil {
   155  		t.Errorf("WriteString: %v", err)
   156  	} else {
   157  		t.Logf("WriteString(%q) = <nil>", part2)
   158  	}
   159  	f.Close()
   160  
   161  	wait(t)
   162  }
   163  
   164  func TestCanLockExistingFile(t *testing.T) {
   165  	t.Parallel()
   166  
   167  	dir, remove := mustTempDir(t)
   168  	defer remove()
   169  	path := filepath.Join(dir, "existing.txt")
   170  
   171  	if err := os.WriteFile(path, []byte("ok"), 0777); err != nil {
   172  		t.Fatalf("os.WriteFile: %v", err)
   173  	}
   174  
   175  	f, err := lockedfile.Edit(path)
   176  	if err != nil {
   177  		t.Fatalf("first Edit: %v", err)
   178  	}
   179  
   180  	wait := mustBlock(t, "Edit", func() {
   181  		other, err := lockedfile.Edit(path)
   182  		if err != nil {
   183  			t.Errorf("second Edit: %v", err)
   184  		}
   185  		other.Close()
   186  	})
   187  
   188  	f.Close()
   189  	wait(t)
   190  }
   191  
   192  // TestSpuriousEDEADLK verifies that the spurious EDEADLK reported in
   193  // https://golang.org/issue/32817 no longer occurs.
   194  func TestSpuriousEDEADLK(t *testing.T) {
   195  	// 	P.1 locks file A.
   196  	// 	Q.3 locks file B.
   197  	// 	Q.3 blocks on file A.
   198  	// 	P.2 blocks on file B. (Spurious EDEADLK occurs here.)
   199  	// 	P.1 unlocks file A.
   200  	// 	Q.3 unblocks and locks file A.
   201  	// 	Q.3 unlocks files A and B.
   202  	// 	P.2 unblocks and locks file B.
   203  	// 	P.2 unlocks file B.
   204  
   205  	testenv.MustHaveExec(t)
   206  
   207  	dirVar := t.Name() + "DIR"
   208  
   209  	if dir := os.Getenv(dirVar); dir != "" {
   210  		// Q.3 locks file B.
   211  		b, err := lockedfile.Edit(filepath.Join(dir, "B"))
   212  		if err != nil {
   213  			t.Fatal(err)
   214  		}
   215  		defer b.Close()
   216  
   217  		if err := os.WriteFile(filepath.Join(dir, "locked"), []byte("ok"), 0666); err != nil {
   218  			t.Fatal(err)
   219  		}
   220  
   221  		// Q.3 blocks on file A.
   222  		a, err := lockedfile.Edit(filepath.Join(dir, "A"))
   223  		// Q.3 unblocks and locks file A.
   224  		if err != nil {
   225  			t.Fatal(err)
   226  		}
   227  		defer a.Close()
   228  
   229  		// Q.3 unlocks files A and B.
   230  		return
   231  	}
   232  
   233  	dir, remove := mustTempDir(t)
   234  	defer remove()
   235  
   236  	// P.1 locks file A.
   237  	a, err := lockedfile.Edit(filepath.Join(dir, "A"))
   238  	if err != nil {
   239  		t.Fatal(err)
   240  	}
   241  
   242  	cmd := testenv.Command(t, os.Args[0], "-test.run=^"+t.Name()+"$")
   243  	cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", dirVar, dir))
   244  
   245  	qDone := make(chan struct{})
   246  	waitQ := mustBlock(t, "Edit A and B in subprocess", func() {
   247  		out, err := cmd.CombinedOutput()
   248  		if err != nil {
   249  			t.Errorf("%v:\n%s", err, out)
   250  		}
   251  		close(qDone)
   252  	})
   253  
   254  	// Wait until process Q has either failed or locked file B.
   255  	// Otherwise, P.2 might not block on file B as intended.
   256  locked:
   257  	for {
   258  		if _, err := os.Stat(filepath.Join(dir, "locked")); !os.IsNotExist(err) {
   259  			break locked
   260  		}
   261  		timer := time.NewTimer(1 * time.Millisecond)
   262  		select {
   263  		case <-qDone:
   264  			timer.Stop()
   265  			break locked
   266  		case <-timer.C:
   267  		}
   268  	}
   269  
   270  	waitP2 := mustBlock(t, "Edit B", func() {
   271  		// P.2 blocks on file B. (Spurious EDEADLK occurs here.)
   272  		b, err := lockedfile.Edit(filepath.Join(dir, "B"))
   273  		// P.2 unblocks and locks file B.
   274  		if err != nil {
   275  			t.Error(err)
   276  			return
   277  		}
   278  		// P.2 unlocks file B.
   279  		b.Close()
   280  	})
   281  
   282  	// P.1 unlocks file A.
   283  	a.Close()
   284  
   285  	waitQ(t)
   286  	waitP2(t)
   287  }