| // 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() | |
| } | |
| }) | |
| } | |
| } |