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

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

View as plain text