From d01ed36118650a0ad3cc2332484c84009ed68d32 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 4 Aug 2026 17:42:30 -0700 Subject: [PATCH] test: cover delete-on-close on the windows mount (#10561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: cover delete-on-close on the windows mount Windows software creates temporaries with FILE_FLAG_DELETE_ON_CLOSE and never deletes them explicitly. The conformance suite showed a file outliving its last handle — an aborted test left its file behind and every later test hit a name collision — but nothing in this suite asks for the flag, because os offers no way to. Skips where the flag is unavailable rather than passing quietly. * test: fail delete-on-close on a real error instead of skipping Skipping on any error meant a refused flag looked the same as a platform that cannot ask for it, so the test could pass by never running. It now skips only on that one sentinel and reports everything else. Also stops printing a nil error when the file is still there after its last handle closed, and checks the closes it was discarding. --- test/winfsp/deleteonclose.go | 7 +++++ test/winfsp/deleteonclose_other.go | 9 +++++++ test/winfsp/deleteonclose_windows.go | 24 +++++++++++++++++ test/winfsp/semantics_test.go | 39 ++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 test/winfsp/deleteonclose.go create mode 100644 test/winfsp/deleteonclose_other.go create mode 100644 test/winfsp/deleteonclose_windows.go diff --git a/test/winfsp/deleteonclose.go b/test/winfsp/deleteonclose.go new file mode 100644 index 000000000..967d733e6 --- /dev/null +++ b/test/winfsp/deleteonclose.go @@ -0,0 +1,7 @@ +package winfsp + +import "errors" + +// errWindowsOnly marks the platforms with no way to ask for delete-on-close, +// so the test can tell that apart from the flag being refused. +var errWindowsOnly = errors.New("delete-on-close is a windows flag") diff --git a/test/winfsp/deleteonclose_other.go b/test/winfsp/deleteonclose_other.go new file mode 100644 index 000000000..0d328e7cb --- /dev/null +++ b/test/winfsp/deleteonclose_other.go @@ -0,0 +1,9 @@ +//go:build !windows + +package winfsp + +type handle uintptr + +func createDeleteOnClose(path string) (handle, error) { return 0, errWindowsOnly } + +func closeHandle(h handle) error { return errWindowsOnly } diff --git a/test/winfsp/deleteonclose_windows.go b/test/winfsp/deleteonclose_windows.go new file mode 100644 index 000000000..dc93a971a --- /dev/null +++ b/test/winfsp/deleteonclose_windows.go @@ -0,0 +1,24 @@ +package winfsp + +import "golang.org/x/sys/windows" + +// createDeleteOnClose opens a new file that Windows removes when the last +// handle to it closes. Installers and editors use this for temporaries, and +// os has no way to ask for it. +func createDeleteOnClose(path string) (windows.Handle, error) { + p, err := windows.UTF16PtrFromString(path) + if err != nil { + return windows.InvalidHandle, err + } + return windows.CreateFile( + p, + windows.GENERIC_READ|windows.GENERIC_WRITE, + windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, + nil, + windows.CREATE_NEW, + windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_DELETE_ON_CLOSE, + 0, + ) +} + +func closeHandle(h windows.Handle) error { return windows.CloseHandle(h) } diff --git a/test/winfsp/semantics_test.go b/test/winfsp/semantics_test.go index 67c36b8b7..7620ca0ba 100644 --- a/test/winfsp/semantics_test.go +++ b/test/winfsp/semantics_test.go @@ -2,6 +2,7 @@ package winfsp import ( "bytes" + "errors" "fmt" "os" "path/filepath" @@ -461,3 +462,41 @@ func TestStatfsIsSelfConsistent(t *testing.T) { t.Fatalf("total free (%d) exceeds total (%d)", totalFree, total) } } + +// TestDeleteOnClose covers the pattern Windows software uses for temporaries: +// the file goes away when the last handle closes, with no explicit delete. A +// filesystem that ignores the flag leaves the name behind, and the next +// program to create it gets a collision. +func TestDeleteOnClose(t *testing.T) { + dir := testRoot(t) + path := filepath.Join(dir, "ephemeral.tmp") + + h, err := createDeleteOnClose(path) + if err != nil { + if errors.Is(err, errWindowsOnly) { + t.Skip("delete-on-close needs the Win32 create call") + } + t.Fatalf("open with delete-on-close: %v", err) + } + if _, err := os.Stat(path); err != nil { + closeHandle(h) + t.Fatalf("file is not visible while its handle is open: %v", err) + } + if err := closeHandle(h); err != nil { + t.Fatalf("close: %v", err) + } + if _, err := os.Stat(path); err == nil { + t.Fatal("file outlived its last handle") + } else if !os.IsNotExist(err) { + t.Fatalf("stat after close: %v", err) + } + // The name has to be free again, which is what the conformance suite + // tripped over when an aborted test left its file behind. + h, err = createDeleteOnClose(path) + if err != nil { + t.Fatalf("recreating the same name failed: %v", err) + } + if err := closeHandle(h); err != nil { + t.Fatalf("close after recreate: %v", err) + } +}