| // Copyright 2026 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 ( |
| "errors" |
| "internal/poll" |
| "internal/syscall/windows" |
| "io" |
| "os" |
| "path/filepath" |
| "sync" |
| "syscall" |
| "testing" |
| "time" |
| ) |
| |
| func newLazyFile(t testing.TB, overlapped bool) (*os.File, syscall.Handle) { |
| t.Helper() |
| name := filepath.Join(t.TempDir(), "file") |
| if err := os.WriteFile(name, []byte("hello"), 0600); err != nil { |
| t.Fatal(err) |
| } |
| flags := syscall.O_RDWR |
| if overlapped { |
| flags |= windows.O_FILE_FLAG_OVERLAPPED |
| } |
| h := openHandle(t, name, flags) |
| f := os.NewFile(uintptr(h), name) |
| t.Cleanup(func() { |
| if err := f.Close(); err != nil && !errors.Is(err, os.ErrClosed) { |
| t.Error(err) |
| } |
| }) |
| return f, h |
| } |
| |
| func openHandle(t testing.TB, name string, flags int) syscall.Handle { |
| t.Helper() |
| h, err := syscall.Open(name, flags|syscall.O_CLOEXEC, 0600) |
| if err != nil { |
| t.Fatal(err) |
| } |
| return h |
| } |
| |
| func TestNewFileLazyInit(t *testing.T) { |
| t.Parallel() |
| for _, mode := range []string{"sync", "overlapped"} { |
| t.Run(mode, func(t *testing.T) { |
| for _, first := range []string{"read", "seek", "deadline"} { |
| t.Run(first, func(t *testing.T) { |
| t.Parallel() |
| f, _ := newLazyFile(t, mode == "overlapped") |
| var buf [5]byte |
| switch first { |
| case "read": |
| if n, err := f.Read(buf[:]); err != nil || n != len(buf) || string(buf[:]) != "hello" { |
| t.Fatalf("Read = %q, %d, %v", buf, n, err) |
| } |
| case "seek": |
| if off, err := f.Seek(1, io.SeekStart); err != nil || off != 1 { |
| t.Fatalf("Seek = %d, %v", off, err) |
| } |
| if n, err := f.Read(buf[:1]); err != nil || n != 1 || buf[0] != 'e' { |
| t.Fatalf("Read after Seek = %q, %d, %v", buf[:1], n, err) |
| } |
| case "deadline": |
| err := f.SetReadDeadline(time.Now().Add(-time.Second)) |
| if mode == "sync" { |
| if !errors.Is(err, os.ErrNoDeadline) { |
| t.Fatalf("SetReadDeadline = %v; want ErrNoDeadline", err) |
| } |
| } else { |
| if err != nil { |
| t.Fatal(err) |
| } |
| if _, err := f.Read(buf[:1]); !errors.Is(err, os.ErrDeadlineExceeded) { |
| t.Fatalf("Read = %v; want ErrDeadlineExceeded", err) |
| } |
| } |
| } |
| }) |
| } |
| }) |
| } |
| } |
| |
| func TestNewFileBlockedHandle(t *testing.T) { |
| t.Parallel() |
| for _, action := range []string{"close", "readClose", "deadlineControl"} { |
| t.Run(action, func(t *testing.T) { |
| t.Parallel() |
| name := pipeName() |
| writer := newBytePipe(t, name, true) |
| h := openHandle(t, name, syscall.O_RDWR) |
| var wg sync.WaitGroup |
| wg.Go(func() { |
| var buf [1]byte |
| var n uint32 |
| syscall.ReadFile(h, buf[:], &n, nil) |
| }) |
| time.Sleep(20 * time.Millisecond) // Let the native read block. |
| f := os.NewFile(uintptr(h), name) |
| defer func() { |
| writer.Close() |
| syscall.CancelIoEx(h, nil) |
| wg.Wait() |
| f.Close() |
| }() |
| |
| if action == "readClose" { |
| wg.Go(func() { |
| var buf [1]byte |
| _, err := f.Read(buf[:]) |
| if !errors.Is(err, os.ErrClosed) { |
| t.Errorf("Read = %v; want ErrClosed", err) |
| } |
| }) |
| time.Sleep(20 * time.Millisecond) // Let first-use detection block. |
| } |
| if action == "deadlineControl" { |
| // Unlike Read, a deadline setter does not hold the I/O locks. |
| // Fd must also avoid waiting for the initialization lock. |
| wg.Go(func() { |
| if err := f.SetDeadline(time.Time{}); !errors.Is(err, os.ErrNoDeadline) { |
| t.Errorf("SetDeadline = %v; want ErrNoDeadline", err) |
| } |
| }) |
| time.Sleep(20 * time.Millisecond) // Let first-use detection block. |
| } |
| if action == "close" || action == "readClose" { |
| if err := f.Close(); err != nil { |
| t.Fatal(err) |
| } |
| return |
| } |
| if got := f.Fd(); got != uintptr(h) { |
| t.Errorf("Fd = %d; want %d", got, h) |
| } |
| raw, err := f.SyscallConn() |
| if err == nil { |
| err = raw.Control(func(fd uintptr) { |
| if err := syscall.CancelIoEx(syscall.Handle(fd), nil); err != nil && err != syscall.ERROR_NOT_FOUND { |
| t.Error(err) |
| } |
| }) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| }) |
| } |
| } |
| |
| func TestNewFileEventFallback(t *testing.T) { |
| t.Parallel() |
| for _, fallback := range []string{"fd", "externalIOCP"} { |
| t.Run(fallback, func(t *testing.T) { |
| t.Parallel() |
| f, h := newLazyFile(t, true) |
| if fallback == "fd" { |
| if got := f.Fd(); got != uintptr(h) { |
| t.Fatalf("Fd = %d; want %d", got, h) |
| } |
| } else { |
| iocp, err := windows.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer syscall.CloseHandle(iocp) |
| if _, err := windows.CreateIoCompletionPort(h, iocp, 0, 0); err != nil { |
| t.Fatal(err) |
| } |
| } |
| if err := f.SetDeadline(time.Time{}); !errors.Is(err, os.ErrNoDeadline) { |
| t.Fatalf("SetDeadline = %v; want ErrNoDeadline", err) |
| } |
| var buf [5]byte |
| if n, err := f.ReadAt(buf[:], 0); err != nil || n != len(buf) || string(buf[:]) != "hello" { |
| t.Fatalf("ReadAt = %q, %d, %v", buf, n, err) |
| } |
| }) |
| } |
| } |
| |
| func TestNewFileLazyInitRace(t *testing.T) { |
| t.Parallel() |
| for _, action := range []string{"close", "fd"} { |
| t.Run(action, func(t *testing.T) { |
| for range 100 { |
| f, _ := newLazyFile(t, true) |
| start := make(chan struct{}) |
| var wg sync.WaitGroup |
| for _, op := range []func() error{ |
| func() error { |
| var b [1]byte |
| _, err := f.ReadAt(b[:], 0) |
| return err |
| }, |
| func() error { return f.SetDeadline(time.Time{}) }, |
| } { |
| wg.Go(func() { |
| <-start |
| if err := op(); err != nil && !errors.Is(err, os.ErrClosed) && !errors.Is(err, os.ErrNoDeadline) && err != poll.ErrFileClosing { |
| t.Error(err) |
| } |
| }) |
| } |
| wg.Go(func() { |
| <-start |
| if action == "close" { |
| if err := f.Close(); err != nil { |
| t.Error(err) |
| } |
| } else { |
| f.Fd() |
| } |
| }) |
| close(start) |
| wg.Wait() |
| f.Close() |
| } |
| }) |
| } |
| } |