os: test overlapped pipes deadlines on Windows

NewFile recently added support for overlapped I/O on Windows,
which allows us to set deadlines on them, but the test coverage for
this new feature is not exhaustive.

Modify the existing pipe deadline tests to also exercise named
overlapped pipes.

Updates #19098.

Cq-Include-Trybots: luci.golang.try:gotip-windows-amd64-race,gotip-windows-amd64-longtest,gotip-windows-arm64
Change-Id: I86d284d9fb054c24959045a922cf84feeda5b5f0
Reviewed-on: https://go-review.googlesource.com/c/go/+/668095
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
This commit is contained in:
qmuntal 2025-04-25 10:12:28 +02:00 committed by Quim Muntal
parent 06751c455d
commit 3cebfb678b
3 changed files with 463 additions and 367 deletions

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !js && !plan9 && !wasip1 && !windows //go:build !js && !plan9 && !wasip1
package os_test package os_test
@ -11,18 +11,16 @@ import (
"io" "io"
"math/rand" "math/rand"
"os" "os"
"os/signal"
"runtime" "runtime"
"sync" "sync"
"syscall"
"testing" "testing"
"time" "time"
) )
func TestNonpollableDeadline(t *testing.T) { func TestNonpollableDeadline(t *testing.T) {
// On BSD systems regular files seem to be pollable, // On BSD systems regular files seem to be pollable,
// so just run this test on Linux. // so just run this test on Linux and Windows.
if runtime.GOOS != "linux" { if runtime.GOOS != "linux" && runtime.GOOS != "windows" {
t.Skipf("skipping on %s", runtime.GOOS) t.Skipf("skipping on %s", runtime.GOOS)
} }
t.Parallel() t.Parallel()
@ -45,6 +43,13 @@ func TestNonpollableDeadline(t *testing.T) {
} }
} }
type pipeDeadlineTest struct {
name string
create func(t *testing.T) (r, w *os.File)
}
var pipeDeadlinesTestCases []pipeDeadlineTest
// noDeadline is a zero time.Time value, which cancels a deadline. // noDeadline is a zero time.Time value, which cancels a deadline.
var noDeadline time.Time var noDeadline time.Time
@ -63,40 +68,43 @@ var readTimeoutTests = []struct {
func TestReadTimeout(t *testing.T) { func TestReadTimeout(t *testing.T) {
t.Parallel() t.Parallel()
r, w, err := os.Pipe() for _, tc := range pipeDeadlinesTestCases {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatal(err) t.Parallel()
}
defer r.Close()
defer w.Close()
if _, err := w.Write([]byte("READ TIMEOUT TEST")); err != nil { r, w := tc.create(t)
t.Fatal(err) defer r.Close()
} defer w.Close()
for i, tt := range readTimeoutTests { if _, err := w.Write([]byte("READ TIMEOUT TEST")); err != nil {
if err := r.SetReadDeadline(time.Now().Add(tt.timeout)); err != nil { t.Fatal(err)
t.Fatalf("#%d: %v", i, err) }
}
var b [1]byte for i, tt := range readTimeoutTests {
for j, xerr := range tt.xerrs { if err := r.SetReadDeadline(time.Now().Add(tt.timeout)); err != nil {
for { t.Fatalf("#%d: %v", i, err)
n, err := r.Read(b[:]) }
if xerr != nil { var b [1]byte
if !isDeadlineExceeded(err) { for j, xerr := range tt.xerrs {
t.Fatalf("#%d/%d: %v", i, j, err) for {
n, err := r.Read(b[:])
if xerr != nil {
if !isDeadlineExceeded(err) {
t.Fatalf("#%d/%d: %v", i, j, err)
}
}
if err == nil {
time.Sleep(tt.timeout / 3)
continue
}
if n != 0 {
t.Fatalf("#%d/%d: read %d; want 0", i, j, n)
}
break
} }
} }
if err == nil {
time.Sleep(tt.timeout / 3)
continue
}
if n != 0 {
t.Fatalf("#%d/%d: read %d; want 0", i, j, n)
}
break
} }
} })
} }
} }
@ -104,40 +112,43 @@ func TestReadTimeout(t *testing.T) {
func TestReadTimeoutMustNotReturn(t *testing.T) { func TestReadTimeoutMustNotReturn(t *testing.T) {
t.Parallel() t.Parallel()
r, w, err := os.Pipe() for _, tc := range pipeDeadlinesTestCases {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatal(err) t.Parallel()
}
defer r.Close()
defer w.Close()
max := time.NewTimer(100 * time.Millisecond) r, w := tc.create(t)
defer max.Stop() defer r.Close()
ch := make(chan error) defer w.Close()
go func() {
if err := r.SetDeadline(time.Now().Add(-5 * time.Second)); err != nil {
t.Error(err)
}
if err := r.SetWriteDeadline(time.Now().Add(-5 * time.Second)); err != nil {
t.Error(err)
}
if err := r.SetReadDeadline(noDeadline); err != nil {
t.Error(err)
}
var b [1]byte
_, err := r.Read(b[:])
ch <- err
}()
select { max := time.NewTimer(100 * time.Millisecond)
case err := <-ch: defer max.Stop()
t.Fatalf("expected Read to not return, but it returned with %v", err) ch := make(chan error)
case <-max.C: go func() {
w.Close() if err := r.SetDeadline(time.Now().Add(-5 * time.Second)); err != nil {
err := <-ch // wait for tester goroutine to stop t.Error(err)
if os.IsTimeout(err) { }
t.Fatal(err) if err := r.SetWriteDeadline(time.Now().Add(-5 * time.Second)); err != nil {
} t.Error(err)
}
if err := r.SetReadDeadline(noDeadline); err != nil {
t.Error(err)
}
var b [1]byte
_, err := r.Read(b[:])
ch <- err
}()
select {
case err := <-ch:
t.Fatalf("expected Read to not return, but it returned with %v", err)
case <-max.C:
w.Close()
err := <-ch // wait for tester goroutine to stop
if os.IsTimeout(err) {
t.Fatal(err)
}
}
})
} }
} }
@ -156,35 +167,38 @@ var writeTimeoutTests = []struct {
func TestWriteTimeout(t *testing.T) { func TestWriteTimeout(t *testing.T) {
t.Parallel() t.Parallel()
for i, tt := range writeTimeoutTests { for _, tc := range pipeDeadlinesTestCases {
t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r, w, err := os.Pipe() t.Parallel()
if err != nil {
t.Fatal(err)
}
defer r.Close()
defer w.Close()
if err := w.SetWriteDeadline(time.Now().Add(tt.timeout)); err != nil { for i, tt := range writeTimeoutTests {
t.Fatalf("%v", err) t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
} r, w := tc.create(t)
for j, xerr := range tt.xerrs { defer r.Close()
for { defer w.Close()
n, err := w.Write([]byte("WRITE TIMEOUT TEST"))
if xerr != nil { if err := w.SetWriteDeadline(time.Now().Add(tt.timeout)); err != nil {
if !isDeadlineExceeded(err) { t.Fatalf("%v", err)
t.Fatalf("%d: %v", j, err) }
for j, xerr := range tt.xerrs {
for {
n, err := w.Write([]byte("WRITE TIMEOUT TEST"))
if xerr != nil {
if !isDeadlineExceeded(err) {
t.Fatalf("%d: %v", j, err)
}
}
if err == nil {
time.Sleep(tt.timeout / 3)
continue
}
if n != 0 {
t.Fatalf("%d: wrote %d; want 0", j, n)
}
break
} }
} }
if err == nil { })
time.Sleep(tt.timeout / 3)
continue
}
if n != 0 {
t.Fatalf("%d: wrote %d; want 0", j, n)
}
break
}
} }
}) })
} }
@ -194,44 +208,47 @@ func TestWriteTimeout(t *testing.T) {
func TestWriteTimeoutMustNotReturn(t *testing.T) { func TestWriteTimeoutMustNotReturn(t *testing.T) {
t.Parallel() t.Parallel()
r, w, err := os.Pipe() for _, tc := range pipeDeadlinesTestCases {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatal(err) t.Parallel()
}
defer r.Close()
defer w.Close()
max := time.NewTimer(100 * time.Millisecond) r, w := tc.create(t)
defer max.Stop() defer r.Close()
ch := make(chan error) defer w.Close()
go func() {
if err := w.SetDeadline(time.Now().Add(-5 * time.Second)); err != nil { max := time.NewTimer(100 * time.Millisecond)
t.Error(err) defer max.Stop()
} ch := make(chan error)
if err := w.SetReadDeadline(time.Now().Add(-5 * time.Second)); err != nil { go func() {
t.Error(err) if err := w.SetDeadline(time.Now().Add(-5 * time.Second)); err != nil {
} t.Error(err)
if err := w.SetWriteDeadline(noDeadline); err != nil { }
t.Error(err) if err := w.SetReadDeadline(time.Now().Add(-5 * time.Second)); err != nil {
} t.Error(err)
var b [1]byte }
for { if err := w.SetWriteDeadline(noDeadline); err != nil {
if _, err := w.Write(b[:]); err != nil { t.Error(err)
ch <- err }
break var b [1]byte
for {
if _, err := w.Write(b[:]); err != nil {
ch <- err
break
}
}
}()
select {
case err := <-ch:
t.Fatalf("expected Write to not return, but it returned with %v", err)
case <-max.C:
r.Close()
err := <-ch // wait for tester goroutine to stop
if os.IsTimeout(err) {
t.Fatal(err)
}
} }
} })
}()
select {
case err := <-ch:
t.Fatalf("expected Write to not return, but it returned with %v", err)
case <-max.C:
r.Close()
err := <-ch // wait for tester goroutine to stop
if os.IsTimeout(err) {
t.Fatal(err)
}
} }
} }
@ -289,50 +306,53 @@ func nextTimeout(actual time.Duration) (next time.Duration, ok bool) {
func TestReadTimeoutFluctuation(t *testing.T) { func TestReadTimeoutFluctuation(t *testing.T) {
t.Parallel() t.Parallel()
r, w, err := os.Pipe() for _, tc := range pipeDeadlinesTestCases {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatal(err) t.Parallel()
}
defer r.Close()
defer w.Close()
d := minDynamicTimeout r, w := tc.create(t)
b := make([]byte, 256) defer r.Close()
for { defer w.Close()
t.Logf("SetReadDeadline(+%v)", d)
t0 := time.Now()
deadline := t0.Add(d)
if err = r.SetReadDeadline(deadline); err != nil {
t.Fatalf("SetReadDeadline(%v): %v", deadline, err)
}
var n int
n, err = r.Read(b)
t1 := time.Now()
if n != 0 || err == nil || !isDeadlineExceeded(err) { d := minDynamicTimeout
t.Errorf("Read did not return (0, timeout): (%d, %v)", n, err) b := make([]byte, 256)
} for {
t.Logf("SetReadDeadline(+%v)", d)
t0 := time.Now()
deadline := t0.Add(d)
if err := r.SetReadDeadline(deadline); err != nil {
t.Fatalf("SetReadDeadline(%v): %v", deadline, err)
}
var n int
n, err := r.Read(b)
t1 := time.Now()
actual := t1.Sub(t0) if n != 0 || err == nil || !isDeadlineExceeded(err) {
if t1.Before(deadline) { t.Errorf("Read did not return (0, timeout): (%d, %v)", n, err)
t.Errorf("Read took %s; expected at least %s", actual, d) }
}
if t.Failed() { actual := t1.Sub(t0)
return if t1.Before(deadline) {
} t.Errorf("Read took %s; expected at least %s", actual, d)
if want := timeoutUpperBound(d); actual > want { }
next, ok := nextTimeout(actual) if t.Failed() {
if !ok { return
t.Fatalf("Read took %s; expected at most %v", actual, want) }
if want := timeoutUpperBound(d); actual > want {
next, ok := nextTimeout(actual)
if !ok {
t.Fatalf("Read took %s; expected at most %v", actual, want)
}
// Maybe this machine is too slow to reliably schedule goroutines within
// the requested duration. Increase the timeout and try again.
t.Logf("Read took %s (expected %s); trying with longer timeout", actual, d)
d = next
continue
}
break
} }
// Maybe this machine is too slow to reliably schedule goroutines within })
// the requested duration. Increase the timeout and try again.
t.Logf("Read took %s (expected %s); trying with longer timeout", actual, d)
d = next
continue
}
break
} }
} }
@ -340,76 +360,84 @@ func TestReadTimeoutFluctuation(t *testing.T) {
func TestWriteTimeoutFluctuation(t *testing.T) { func TestWriteTimeoutFluctuation(t *testing.T) {
t.Parallel() t.Parallel()
r, w, err := os.Pipe() for _, tc := range pipeDeadlinesTestCases {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatal(err) t.Parallel()
}
defer r.Close() r, w := tc.create(t)
defer w.Close() defer r.Close()
defer w.Close()
d := minDynamicTimeout
for {
t.Logf("SetWriteDeadline(+%v)", d)
t0 := time.Now()
deadline := t0.Add(d)
if err := w.SetWriteDeadline(deadline); err != nil {
t.Fatalf("SetWriteDeadline(%v): %v", deadline, err)
}
var n int64
var err error
for {
var dn int
dn, err = w.Write([]byte("TIMEOUT TRANSMITTER"))
n += int64(dn)
if err != nil {
break
}
}
t1 := time.Now()
// Inv: err != nil
if !isDeadlineExceeded(err) {
t.Fatalf("Write did not return (any, timeout): (%d, %v)", n, err)
}
actual := t1.Sub(t0)
if t1.Before(deadline) {
t.Errorf("Write took %s; expected at least %s", actual, d)
}
if t.Failed() {
return
}
if want := timeoutUpperBound(d); actual > want {
if n > 0 {
// SetWriteDeadline specifies a time “after which I/O operations fail
// instead of blocking”. However, the kernel's send buffer is not yet
// full, we may be able to write some arbitrary (but finite) number of
// bytes to it without blocking.
t.Logf("Wrote %d bytes into send buffer; retrying until buffer is full", n)
if d <= maxDynamicTimeout/2 {
// We don't know how long the actual write loop would have taken if
// the buffer were full, so just guess and double the duration so that
// the next attempt can make twice as much progress toward filling it.
d *= 2
}
} else if next, ok := nextTimeout(actual); !ok {
t.Fatalf("Write took %s; expected at most %s", actual, want)
} else {
// Maybe this machine is too slow to reliably schedule goroutines within
// the requested duration. Increase the timeout and try again.
t.Logf("Write took %s (expected %s); trying with longer timeout", actual, d)
d = next
}
continue
}
d := minDynamicTimeout
for {
t.Logf("SetWriteDeadline(+%v)", d)
t0 := time.Now()
deadline := t0.Add(d)
if err := w.SetWriteDeadline(deadline); err != nil {
t.Fatalf("SetWriteDeadline(%v): %v", deadline, err)
}
var n int64
var err error
for {
var dn int
dn, err = w.Write([]byte("TIMEOUT TRANSMITTER"))
n += int64(dn)
if err != nil {
break break
} }
} })
t1 := time.Now()
// Inv: err != nil
if !isDeadlineExceeded(err) {
t.Fatalf("Write did not return (any, timeout): (%d, %v)", n, err)
}
actual := t1.Sub(t0)
if t1.Before(deadline) {
t.Errorf("Write took %s; expected at least %s", actual, d)
}
if t.Failed() {
return
}
if want := timeoutUpperBound(d); actual > want {
if n > 0 {
// SetWriteDeadline specifies a time “after which I/O operations fail
// instead of blocking”. However, the kernel's send buffer is not yet
// full, we may be able to write some arbitrary (but finite) number of
// bytes to it without blocking.
t.Logf("Wrote %d bytes into send buffer; retrying until buffer is full", n)
if d <= maxDynamicTimeout/2 {
// We don't know how long the actual write loop would have taken if
// the buffer were full, so just guess and double the duration so that
// the next attempt can make twice as much progress toward filling it.
d *= 2
}
} else if next, ok := nextTimeout(actual); !ok {
t.Fatalf("Write took %s; expected at most %s", actual, want)
} else {
// Maybe this machine is too slow to reliably schedule goroutines within
// the requested duration. Increase the timeout and try again.
t.Logf("Write took %s (expected %s); trying with longer timeout", actual, d)
d = next
}
continue
}
break
} }
} }
// There is a very similar copy of this in net/timeout_test.go. // There is a very similar copy of this in net/timeout_test.go.
func TestVariousDeadlines(t *testing.T) { func TestVariousDeadlines(t *testing.T) {
t.Parallel() t.Parallel()
testVariousDeadlines(t) for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
testVariousDeadlines(t, tc.create)
})
}
} }
// There is a very similar copy of this in net/timeout_test.go. // There is a very similar copy of this in net/timeout_test.go.
@ -419,7 +447,12 @@ func TestVariousDeadlines1Proc(t *testing.T) {
t.Skip("skipping in short mode") t.Skip("skipping in short mode")
} }
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
testVariousDeadlines(t) for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
testVariousDeadlines(t, tc.create)
})
}
} }
// There is a very similar copy of this in net/timeout_test.go. // There is a very similar copy of this in net/timeout_test.go.
@ -429,7 +462,12 @@ func TestVariousDeadlines4Proc(t *testing.T) {
t.Skip("skipping in short mode") t.Skip("skipping in short mode")
} }
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4))
testVariousDeadlines(t) for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
testVariousDeadlines(t, tc.create)
})
}
} }
type neverEnding byte type neverEnding byte
@ -441,7 +479,7 @@ func (b neverEnding) Read(p []byte) (int, error) {
return len(p), nil return len(p), nil
} }
func testVariousDeadlines(t *testing.T) { func testVariousDeadlines(t *testing.T, create func(t *testing.T) (r, w *os.File)) {
type result struct { type result struct {
n int64 n int64
err error err error
@ -487,10 +525,7 @@ func testVariousDeadlines(t *testing.T) {
} }
for run := 0; run < numRuns; run++ { for run := 0; run < numRuns; run++ {
t.Run(fmt.Sprintf("%v-%d", timeout, run+1), func(t *testing.T) { t.Run(fmt.Sprintf("%v-%d", timeout, run+1), func(t *testing.T) {
r, w, err := os.Pipe() r, w := create(t)
if err != nil {
t.Fatal(err)
}
defer r.Close() defer r.Close()
defer w.Close() defer w.Close()
@ -514,7 +549,7 @@ func testVariousDeadlines(t *testing.T) {
select { select {
case res := <-actvch: case res := <-actvch:
if !isDeadlineExceeded(err) { if isDeadlineExceeded(res.err) {
t.Logf("good client timeout after %v, reading %d bytes", res.d, res.n) t.Logf("good client timeout after %v, reading %d bytes", res.d, res.n)
} else { } else {
t.Fatalf("client Copy = %d, %v; want timeout", res.n, res.err) t.Fatalf("client Copy = %d, %v; want timeout", res.n, res.err)
@ -543,50 +578,53 @@ func TestReadWriteDeadlineRace(t *testing.T) {
N = 50 N = 50
} }
r, w, err := os.Pipe() for _, tc := range pipeDeadlinesTestCases {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatal(err) t.Parallel()
}
defer r.Close()
defer w.Close()
var wg sync.WaitGroup r, w := tc.create(t)
wg.Add(3) defer r.Close()
go func() { defer w.Close()
defer wg.Done()
tic := time.NewTicker(2 * time.Microsecond) var wg sync.WaitGroup
defer tic.Stop() wg.Add(3)
for i := 0; i < N; i++ { go func() {
if err := r.SetReadDeadline(time.Now().Add(2 * time.Microsecond)); err != nil { defer wg.Done()
break tic := time.NewTicker(2 * time.Microsecond)
} defer tic.Stop()
if err := w.SetWriteDeadline(time.Now().Add(2 * time.Microsecond)); err != nil { for i := 0; i < N; i++ {
break if err := r.SetReadDeadline(time.Now().Add(2 * time.Microsecond)); err != nil {
} break
<-tic.C }
} if err := w.SetWriteDeadline(time.Now().Add(2 * time.Microsecond)); err != nil {
}() break
go func() { }
defer wg.Done() <-tic.C
var b [1]byte }
for i := 0; i < N; i++ { }()
_, err := r.Read(b[:]) go func() {
if err != nil && !isDeadlineExceeded(err) { defer wg.Done()
t.Error("Read returned non-timeout error", err) var b [1]byte
} for i := 0; i < N; i++ {
} _, err := r.Read(b[:])
}() if err != nil && !isDeadlineExceeded(err) {
go func() { t.Error("Read returned non-timeout error", err)
defer wg.Done() }
var b [1]byte }
for i := 0; i < N; i++ { }()
_, err := w.Write(b[:]) go func() {
if err != nil && !isDeadlineExceeded(err) { defer wg.Done()
t.Error("Write returned non-timeout error", err) var b [1]byte
} for i := 0; i < N; i++ {
} _, err := w.Write(b[:])
}() if err != nil && !isDeadlineExceeded(err) {
wg.Wait() // wait for tester goroutine to stop t.Error("Write returned non-timeout error", err)
}
}
}()
wg.Wait() // wait for tester goroutine to stop
})
}
} }
// TestRacyRead tests that it is safe to mutate the input Read buffer // TestRacyRead tests that it is safe to mutate the input Read buffer
@ -594,37 +632,40 @@ func TestReadWriteDeadlineRace(t *testing.T) {
func TestRacyRead(t *testing.T) { func TestRacyRead(t *testing.T) {
t.Parallel() t.Parallel()
r, w, err := os.Pipe() for _, tc := range pipeDeadlinesTestCases {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatal(err) t.Parallel()
}
defer r.Close()
defer w.Close()
var wg sync.WaitGroup r, w := tc.create(t)
defer wg.Wait() defer r.Close()
defer w.Close()
go io.Copy(w, rand.New(rand.NewSource(0))) var wg sync.WaitGroup
defer wg.Wait()
r.SetReadDeadline(time.Now().Add(time.Millisecond)) go io.Copy(w, rand.New(rand.NewSource(0)))
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
b1 := make([]byte, 1024) r.SetReadDeadline(time.Now().Add(time.Millisecond))
b2 := make([]byte, 1024) for i := 0; i < 10; i++ {
for j := 0; j < 100; j++ { wg.Add(1)
_, err := r.Read(b1) go func() {
copy(b1, b2) // Mutate b1 to trigger potential race defer wg.Done()
if err != nil {
if !isDeadlineExceeded(err) { b1 := make([]byte, 1024)
t.Error(err) b2 := make([]byte, 1024)
for j := 0; j < 100; j++ {
_, err := r.Read(b1)
copy(b1, b2) // Mutate b1 to trigger potential race
if err != nil {
if !isDeadlineExceeded(err) {
t.Error(err)
}
r.SetReadDeadline(time.Now().Add(time.Millisecond))
}
} }
r.SetReadDeadline(time.Now().Add(time.Millisecond)) }()
}
} }
}() })
} }
} }
@ -633,73 +674,39 @@ func TestRacyRead(t *testing.T) {
func TestRacyWrite(t *testing.T) { func TestRacyWrite(t *testing.T) {
t.Parallel() t.Parallel()
r, w, err := os.Pipe() for _, tc := range pipeDeadlinesTestCases {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatal(err) t.Parallel()
}
defer r.Close()
defer w.Close()
var wg sync.WaitGroup r, w := tc.create(t)
defer wg.Wait() defer r.Close()
defer w.Close()
go io.Copy(io.Discard, r) var wg sync.WaitGroup
defer wg.Wait()
w.SetWriteDeadline(time.Now().Add(time.Millisecond)) go io.Copy(io.Discard, r)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
b1 := make([]byte, 1024) w.SetWriteDeadline(time.Now().Add(time.Millisecond))
b2 := make([]byte, 1024) for i := 0; i < 10; i++ {
for j := 0; j < 100; j++ { wg.Add(1)
_, err := w.Write(b1) go func() {
copy(b1, b2) // Mutate b1 to trigger potential race defer wg.Done()
if err != nil {
if !isDeadlineExceeded(err) { b1 := make([]byte, 1024)
t.Error(err) b2 := make([]byte, 1024)
for j := 0; j < 100; j++ {
_, err := w.Write(b1)
copy(b1, b2) // Mutate b1 to trigger potential race
if err != nil {
if !isDeadlineExceeded(err) {
t.Error(err)
}
w.SetWriteDeadline(time.Now().Add(time.Millisecond))
}
} }
w.SetWriteDeadline(time.Now().Add(time.Millisecond)) }()
}
} }
}() })
} }
} }
// Closing a TTY while reading from it should not hang. Issue 23943.
func TestTTYClose(t *testing.T) {
// Ignore SIGTTIN in case we are running in the background.
signal.Ignore(syscall.SIGTTIN)
defer signal.Reset(syscall.SIGTTIN)
f, err := os.Open("/dev/tty")
if err != nil {
t.Skipf("skipping because opening /dev/tty failed: %v", err)
}
go func() {
var buf [1]byte
f.Read(buf[:])
}()
// Give the goroutine a chance to enter the read.
// It doesn't matter much if it occasionally fails to do so,
// we won't be testing what we want to test but the test will pass.
time.Sleep(time.Millisecond)
c := make(chan bool)
go func() {
defer close(c)
f.Close()
}()
select {
case <-c:
case <-time.After(time.Second):
t.Error("timed out waiting for close")
}
// On some systems the goroutines may now be hanging.
// There's not much we can do about that.
}

View File

@ -0,0 +1,65 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !js && !plan9 && !wasip1 && !windows
package os_test
import (
"os"
"os/signal"
"syscall"
"testing"
"time"
)
func init() {
pipeDeadlinesTestCases = []pipeDeadlineTest{{
"anonymous pipe",
func(t *testing.T) (r, w *os.File) {
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
return r, w
},
}}
}
// Closing a TTY while reading from it should not hang. Issue 23943.
func TestTTYClose(t *testing.T) {
// Ignore SIGTTIN in case we are running in the background.
signal.Ignore(syscall.SIGTTIN)
defer signal.Reset(syscall.SIGTTIN)
f, err := os.Open("/dev/tty")
if err != nil {
t.Skipf("skipping because opening /dev/tty failed: %v", err)
}
go func() {
var buf [1]byte
f.Read(buf[:])
}()
// Give the goroutine a chance to enter the read.
// It doesn't matter much if it occasionally fails to do so,
// we won't be testing what we want to test but the test will pass.
time.Sleep(time.Millisecond)
c := make(chan bool)
go func() {
defer close(c)
f.Close()
}()
select {
case <-c:
case <-time.After(time.Second):
t.Error("timed out waiting for close")
}
// On some systems the goroutines may now be hanging.
// There's not much we can do about that.
}

View File

@ -0,0 +1,24 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package os_test
import (
"os"
"testing"
)
func init() {
pipeDeadlinesTestCases = []pipeDeadlineTest{
{
"named overlapped pipe",
func(t *testing.T) (r, w *os.File) {
name := pipeName()
w = newBytePipe(t, name, true)
r = newFileOverlapped(t, name, true)
return
},
},
}
}