Black Lives Matter. Support the Equal Justice Initiative.

Source file src/cmd/go/internal/lockedfile/lockedfile_test.go

Documentation: cmd/go/internal/lockedfile

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

View as plain text