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
// license that can be found in the LICENSE file.
//go:build !js && !plan9 && !wasip1 && !windows
//go:build !js && !plan9 && !wasip1
package os_test
@ -11,18 +11,16 @@ import (
"io"
"math/rand"
"os"
"os/signal"
"runtime"
"sync"
"syscall"
"testing"
"time"
)
func TestNonpollableDeadline(t *testing.T) {
// On BSD systems regular files seem to be pollable,
// so just run this test on Linux.
if runtime.GOOS != "linux" {
// so just run this test on Linux and Windows.
if runtime.GOOS != "linux" && runtime.GOOS != "windows" {
t.Skipf("skipping on %s", runtime.GOOS)
}
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.
var noDeadline time.Time
@ -63,10 +68,11 @@ var readTimeoutTests = []struct {
func TestReadTimeout(t *testing.T) {
t.Parallel()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r, w := tc.create(t)
defer r.Close()
defer w.Close()
@ -98,16 +104,19 @@ func TestReadTimeout(t *testing.T) {
}
}
}
})
}
}
// There is a very similar copy of this in net/timeout_test.go.
func TestReadTimeoutMustNotReturn(t *testing.T) {
t.Parallel()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r, w := tc.create(t)
defer r.Close()
defer w.Close()
@ -139,6 +148,8 @@ func TestReadTimeoutMustNotReturn(t *testing.T) {
t.Fatal(err)
}
}
})
}
}
var writeTimeoutTests = []struct {
@ -156,12 +167,13 @@ var writeTimeoutTests = []struct {
func TestWriteTimeout(t *testing.T) {
t.Parallel()
for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
for i, tt := range writeTimeoutTests {
t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
r, w := tc.create(t)
defer r.Close()
defer w.Close()
@ -188,16 +200,19 @@ func TestWriteTimeout(t *testing.T) {
}
})
}
})
}
}
// There is a very similar copy of this in net/timeout_test.go.
func TestWriteTimeoutMustNotReturn(t *testing.T) {
t.Parallel()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r, w := tc.create(t)
defer r.Close()
defer w.Close()
@ -233,6 +248,8 @@ func TestWriteTimeoutMustNotReturn(t *testing.T) {
t.Fatal(err)
}
}
})
}
}
const (
@ -289,10 +306,11 @@ func nextTimeout(actual time.Duration) (next time.Duration, ok bool) {
func TestReadTimeoutFluctuation(t *testing.T) {
t.Parallel()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r, w := tc.create(t)
defer r.Close()
defer w.Close()
@ -302,11 +320,11 @@ func TestReadTimeoutFluctuation(t *testing.T) {
t.Logf("SetReadDeadline(+%v)", d)
t0 := time.Now()
deadline := t0.Add(d)
if err = r.SetReadDeadline(deadline); err != nil {
if err := r.SetReadDeadline(deadline); err != nil {
t.Fatalf("SetReadDeadline(%v): %v", deadline, err)
}
var n int
n, err = r.Read(b)
n, err := r.Read(b)
t1 := time.Now()
if n != 0 || err == nil || !isDeadlineExceeded(err) {
@ -334,16 +352,19 @@ func TestReadTimeoutFluctuation(t *testing.T) {
break
}
})
}
}
// There is a very similar copy of this in net/timeout_test.go.
func TestWriteTimeoutFluctuation(t *testing.T) {
t.Parallel()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r, w := tc.create(t)
defer r.Close()
defer w.Close()
@ -404,12 +425,19 @@ func TestWriteTimeoutFluctuation(t *testing.T) {
break
}
})
}
}
// There is a very similar copy of this in net/timeout_test.go.
func TestVariousDeadlines(t *testing.T) {
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.
@ -419,7 +447,12 @@ func TestVariousDeadlines1Proc(t *testing.T) {
t.Skip("skipping in short mode")
}
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.
@ -429,7 +462,12 @@ func TestVariousDeadlines4Proc(t *testing.T) {
t.Skip("skipping in short mode")
}
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
@ -441,7 +479,7 @@ func (b neverEnding) Read(p []byte) (int, error) {
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 {
n int64
err error
@ -487,10 +525,7 @@ func testVariousDeadlines(t *testing.T) {
}
for run := 0; run < numRuns; run++ {
t.Run(fmt.Sprintf("%v-%d", timeout, run+1), func(t *testing.T) {
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
r, w := create(t)
defer r.Close()
defer w.Close()
@ -514,7 +549,7 @@ func testVariousDeadlines(t *testing.T) {
select {
case res := <-actvch:
if !isDeadlineExceeded(err) {
if isDeadlineExceeded(res.err) {
t.Logf("good client timeout after %v, reading %d bytes", res.d, res.n)
} else {
t.Fatalf("client Copy = %d, %v; want timeout", res.n, res.err)
@ -543,10 +578,11 @@ func TestReadWriteDeadlineRace(t *testing.T) {
N = 50
}
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r, w := tc.create(t)
defer r.Close()
defer w.Close()
@ -587,6 +623,8 @@ func TestReadWriteDeadlineRace(t *testing.T) {
}
}()
wg.Wait() // wait for tester goroutine to stop
})
}
}
// TestRacyRead tests that it is safe to mutate the input Read buffer
@ -594,10 +632,11 @@ func TestReadWriteDeadlineRace(t *testing.T) {
func TestRacyRead(t *testing.T) {
t.Parallel()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r, w := tc.create(t)
defer r.Close()
defer w.Close()
@ -626,6 +665,8 @@ func TestRacyRead(t *testing.T) {
}
}()
}
})
}
}
// TestRacyWrite tests that it is safe to mutate the input Write buffer
@ -633,10 +674,11 @@ func TestRacyRead(t *testing.T) {
func TestRacyWrite(t *testing.T) {
t.Parallel()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
for _, tc := range pipeDeadlinesTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r, w := tc.create(t)
defer r.Close()
defer w.Close()
@ -665,41 +707,6 @@ func TestRacyWrite(t *testing.T) {
}
}()
}
})
}
// 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
},
},
}
}