aboutsummaryrefslogtreecommitdiff
path: root/test/fuse_integration/posix_compliance_test.go
diff options
context:
space:
mode:
authorchrislu <chris.lu@gmail.com>2025-08-30 20:21:38 -0700
committerchrislu <chris.lu@gmail.com>2025-08-30 20:21:38 -0700
commit478d550cba30affd2f4d211317bdfe3352a96e29 (patch)
treeb63a293c2605b459a8cdbd007789d4b886eb2021 /test/fuse_integration/posix_compliance_test.go
parent879d512b552d834136cfb746a239e6168e5c4ffb (diff)
downloadseaweedfs-478d550cba30affd2f4d211317bdfe3352a96e29.tar.xz
seaweedfs-478d550cba30affd2f4d211317bdfe3352a96e29.zip
add posix tests
Diffstat (limited to 'test/fuse_integration/posix_compliance_test.go')
-rw-r--r--test/fuse_integration/posix_compliance_test.go663
1 files changed, 663 insertions, 0 deletions
diff --git a/test/fuse_integration/posix_compliance_test.go b/test/fuse_integration/posix_compliance_test.go
new file mode 100644
index 000000000..9a9fb152f
--- /dev/null
+++ b/test/fuse_integration/posix_compliance_test.go
@@ -0,0 +1,663 @@
+package fuse_test
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+// POSIXComplianceTestSuite provides comprehensive POSIX compliance testing for FUSE mounts
+type POSIXComplianceTestSuite struct {
+ framework *FuseTestFramework
+ t *testing.T
+}
+
+// NewPOSIXComplianceTestSuite creates a new POSIX compliance test suite
+func NewPOSIXComplianceTestSuite(t *testing.T, framework *FuseTestFramework) *POSIXComplianceTestSuite {
+ return &POSIXComplianceTestSuite{
+ framework: framework,
+ t: t,
+ }
+}
+
+// TestPOSIXCompliance runs all POSIX compliance tests
+func TestPOSIXCompliance(t *testing.T) {
+ config := DefaultTestConfig()
+ config.EnableDebug = true
+ config.MountOptions = []string{"-allowOthers", "-nonempty"}
+
+ framework := NewFuseTestFramework(t, config)
+ defer framework.Cleanup()
+ require.NoError(t, framework.Setup(config))
+
+ suite := NewPOSIXComplianceTestSuite(t, framework)
+
+ // Run all POSIX compliance test categories
+ t.Run("FileOperations", suite.TestFileOperations)
+ t.Run("DirectoryOperations", suite.TestDirectoryOperations)
+ t.Run("SymlinkOperations", suite.TestSymlinkOperations)
+ t.Run("PermissionTests", suite.TestPermissions)
+ t.Run("TimestampTests", suite.TestTimestamps)
+ t.Run("IOOperations", suite.TestIOOperations)
+ t.Run("FileDescriptorTests", suite.TestFileDescriptors)
+ t.Run("AtomicOperations", suite.TestAtomicOperations)
+ t.Run("ConcurrentAccess", suite.TestConcurrentAccess)
+ t.Run("ErrorHandling", suite.TestErrorHandling)
+}
+
+// TestFileOperations tests POSIX file operation compliance
+func (s *POSIXComplianceTestSuite) TestFileOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("CreateFile", func(t *testing.T) {
+ filepath := filepath.Join(mountPoint, "test_create.txt")
+
+ // Test file creation with O_CREAT
+ fd, err := syscall.Open(filepath, syscall.O_CREAT|syscall.O_WRONLY, 0644)
+ require.NoError(t, err)
+ require.Greater(t, fd, 0)
+
+ err = syscall.Close(fd)
+ require.NoError(t, err)
+
+ // Verify file exists
+ _, err = os.Stat(filepath)
+ require.NoError(t, err)
+ })
+
+ t.Run("CreateExclusiveFile", func(t *testing.T) {
+ filepath := filepath.Join(mountPoint, "test_excl.txt")
+
+ // First creation should succeed
+ fd, err := syscall.Open(filepath, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, 0644)
+ require.NoError(t, err)
+ syscall.Close(fd)
+
+ // Second creation should fail with EEXIST
+ _, err = syscall.Open(filepath, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, 0644)
+ require.Error(t, err)
+ require.Equal(t, syscall.EEXIST, err)
+ })
+
+ t.Run("TruncateFile", func(t *testing.T) {
+ filepath := filepath.Join(mountPoint, "test_truncate.txt")
+ content := []byte("Hello, World! This is a test file for truncation.")
+
+ // Create file with content
+ err := os.WriteFile(filepath, content, 0644)
+ require.NoError(t, err)
+
+ // Truncate to 5 bytes
+ err = syscall.Truncate(filepath, 5)
+ require.NoError(t, err)
+
+ // Verify truncation
+ readContent, err := os.ReadFile(filepath)
+ require.NoError(t, err)
+ require.Equal(t, []byte("Hello"), readContent)
+ })
+
+ t.Run("UnlinkFile", func(t *testing.T) {
+ filepath := filepath.Join(mountPoint, "test_unlink.txt")
+
+ // Create file
+ err := os.WriteFile(filepath, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Unlink file
+ err = syscall.Unlink(filepath)
+ require.NoError(t, err)
+
+ // Verify file no longer exists
+ _, err = os.Stat(filepath)
+ require.True(t, os.IsNotExist(err))
+ })
+}
+
+// TestDirectoryOperations tests POSIX directory operation compliance
+func (s *POSIXComplianceTestSuite) TestDirectoryOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("CreateDirectory", func(t *testing.T) {
+ dirPath := filepath.Join(mountPoint, "test_mkdir")
+
+ err := syscall.Mkdir(dirPath, 0755)
+ require.NoError(t, err)
+
+ // Verify directory exists and has correct type
+ stat, err := os.Stat(dirPath)
+ require.NoError(t, err)
+ require.True(t, stat.IsDir())
+ })
+
+ t.Run("RemoveDirectory", func(t *testing.T) {
+ dirPath := filepath.Join(mountPoint, "test_rmdir")
+
+ // Create directory
+ err := os.Mkdir(dirPath, 0755)
+ require.NoError(t, err)
+
+ // Remove directory
+ err = syscall.Rmdir(dirPath)
+ require.NoError(t, err)
+
+ // Verify directory no longer exists
+ _, err = os.Stat(dirPath)
+ require.True(t, os.IsNotExist(err))
+ })
+
+ t.Run("RemoveNonEmptyDirectory", func(t *testing.T) {
+ dirPath := filepath.Join(mountPoint, "test_rmdir_nonempty")
+ filePath := filepath.Join(dirPath, "file.txt")
+
+ // Create directory and file
+ err := os.Mkdir(dirPath, 0755)
+ require.NoError(t, err)
+ err = os.WriteFile(filePath, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Attempt to remove non-empty directory should fail
+ err = syscall.Rmdir(dirPath)
+ require.Error(t, err)
+ require.Equal(t, syscall.ENOTEMPTY, err)
+ })
+
+ t.Run("RenameDirectory", func(t *testing.T) {
+ oldPath := filepath.Join(mountPoint, "old_dir")
+ newPath := filepath.Join(mountPoint, "new_dir")
+
+ // Create directory
+ err := os.Mkdir(oldPath, 0755)
+ require.NoError(t, err)
+
+ // Rename directory
+ err = os.Rename(oldPath, newPath)
+ require.NoError(t, err)
+
+ // Verify old path doesn't exist and new path does
+ _, err = os.Stat(oldPath)
+ require.True(t, os.IsNotExist(err))
+ stat, err := os.Stat(newPath)
+ require.NoError(t, err)
+ require.True(t, stat.IsDir())
+ })
+}
+
+// TestSymlinkOperations tests POSIX symlink operation compliance
+func (s *POSIXComplianceTestSuite) TestSymlinkOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("CreateSymlink", func(t *testing.T) {
+ targetFile := filepath.Join(mountPoint, "target.txt")
+ linkFile := filepath.Join(mountPoint, "link.txt")
+
+ // Create target file
+ err := os.WriteFile(targetFile, []byte("target content"), 0644)
+ require.NoError(t, err)
+
+ // Create symlink
+ err = os.Symlink(targetFile, linkFile)
+ require.NoError(t, err)
+
+ // Verify symlink properties
+ linkStat, err := os.Lstat(linkFile)
+ require.NoError(t, err)
+ require.Equal(t, os.ModeSymlink, linkStat.Mode()&os.ModeType)
+
+ // Verify symlink content
+ linkTarget, err := os.Readlink(linkFile)
+ require.NoError(t, err)
+ require.Equal(t, targetFile, linkTarget)
+
+ // Verify following symlink works
+ content, err := os.ReadFile(linkFile)
+ require.NoError(t, err)
+ require.Equal(t, []byte("target content"), content)
+ })
+
+ t.Run("BrokenSymlink", func(t *testing.T) {
+ nonexistentTarget := filepath.Join(mountPoint, "nonexistent.txt")
+ linkFile := filepath.Join(mountPoint, "broken_link.txt")
+
+ // Create symlink to nonexistent file
+ err := os.Symlink(nonexistentTarget, linkFile)
+ require.NoError(t, err)
+
+ // Lstat should work (doesn't follow symlink)
+ _, err = os.Lstat(linkFile)
+ require.NoError(t, err)
+
+ // Stat should fail (follows symlink to nonexistent target)
+ _, err = os.Stat(linkFile)
+ require.Error(t, err)
+ require.True(t, os.IsNotExist(err))
+ })
+}
+
+// TestPermissions tests POSIX permission compliance
+func (s *POSIXComplianceTestSuite) TestPermissions(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("FilePermissions", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "perm_test.txt")
+
+ // Create file with specific permissions
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_WRONLY, 0642)
+ require.NoError(t, err)
+ syscall.Close(fd)
+
+ // Verify permissions
+ stat, err := os.Stat(testFile)
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0642), stat.Mode()&os.ModePerm)
+ })
+
+ t.Run("ChangeFilePermissions", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "chmod_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Change permissions
+ err = os.Chmod(testFile, 0755)
+ require.NoError(t, err)
+
+ // Verify new permissions
+ stat, err := os.Stat(testFile)
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0755), stat.Mode()&os.ModePerm)
+ })
+
+ t.Run("DirectoryPermissions", func(t *testing.T) {
+ testDir := filepath.Join(mountPoint, "perm_dir")
+
+ // Create directory with specific permissions
+ err := syscall.Mkdir(testDir, 0750)
+ require.NoError(t, err)
+
+ // Verify permissions
+ stat, err := os.Stat(testDir)
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0750)|os.ModeDir, stat.Mode())
+ })
+}
+
+// TestTimestamps tests POSIX timestamp compliance
+func (s *POSIXComplianceTestSuite) TestTimestamps(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("AccessTime", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "atime_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("test content"), 0644)
+ require.NoError(t, err)
+
+ // Get initial timestamps
+ stat1, err := os.Stat(testFile)
+ require.NoError(t, err)
+
+ // Wait a bit to ensure time difference
+ time.Sleep(100 * time.Millisecond)
+
+ // Read file (should update access time)
+ _, err = os.ReadFile(testFile)
+ require.NoError(t, err)
+
+ // Get new timestamps
+ stat2, err := os.Stat(testFile)
+ require.NoError(t, err)
+
+ // Access time should have changed (or at least not be earlier)
+ require.True(t, stat2.ModTime().Equal(stat1.ModTime()) || stat2.ModTime().After(stat1.ModTime()))
+ })
+
+ t.Run("ModificationTime", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "mtime_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("initial content"), 0644)
+ require.NoError(t, err)
+
+ // Get initial timestamp
+ stat1, err := os.Stat(testFile)
+ require.NoError(t, err)
+
+ // Wait to ensure time difference
+ time.Sleep(100 * time.Millisecond)
+
+ // Modify file
+ err = os.WriteFile(testFile, []byte("modified content"), 0644)
+ require.NoError(t, err)
+
+ // Get new timestamp
+ stat2, err := os.Stat(testFile)
+ require.NoError(t, err)
+
+ // Modification time should have changed
+ require.True(t, stat2.ModTime().After(stat1.ModTime()))
+ })
+
+ t.Run("SetTimestamps", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "utime_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Set specific timestamps
+ atime := time.Now().Add(-24 * time.Hour)
+ mtime := time.Now().Add(-12 * time.Hour)
+
+ err = os.Chtimes(testFile, atime, mtime)
+ require.NoError(t, err)
+
+ // Verify timestamps were set
+ stat, err := os.Stat(testFile)
+ require.NoError(t, err)
+ require.True(t, stat.ModTime().Equal(mtime))
+ })
+}
+
+// TestIOOperations tests POSIX I/O operation compliance
+func (s *POSIXComplianceTestSuite) TestIOOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("ReadWrite", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "rw_test.txt")
+ testData := []byte("Hello, POSIX World!")
+
+ // Write data
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_WRONLY, 0644)
+ require.NoError(t, err)
+
+ n, err := syscall.Write(fd, testData)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+
+ err = syscall.Close(fd)
+ require.NoError(t, err)
+
+ // Read data back
+ fd, err = syscall.Open(testFile, syscall.O_RDONLY, 0)
+ require.NoError(t, err)
+
+ readBuffer := make([]byte, len(testData))
+ n, err = syscall.Read(fd, readBuffer)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+ require.Equal(t, testData, readBuffer)
+
+ err = syscall.Close(fd)
+ require.NoError(t, err)
+ })
+
+ t.Run("Seek", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "seek_test.txt")
+ testData := []byte("0123456789ABCDEF")
+
+ // Create file with test data
+ err := os.WriteFile(testFile, testData, 0644)
+ require.NoError(t, err)
+
+ // Open for reading
+ fd, err := syscall.Open(testFile, syscall.O_RDONLY, 0)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Seek to position 5
+ pos, err := syscall.Seek(fd, 5, 0) // SEEK_SET
+ require.NoError(t, err)
+ require.Equal(t, int64(5), pos)
+
+ // Read 3 bytes
+ buffer := make([]byte, 3)
+ n, err := syscall.Read(fd, buffer)
+ require.NoError(t, err)
+ require.Equal(t, 3, n)
+ require.Equal(t, []byte("567"), buffer)
+
+ // Seek from current position
+ pos, err = syscall.Seek(fd, 2, 1) // SEEK_CUR
+ require.NoError(t, err)
+ require.Equal(t, int64(10), pos)
+
+ // Read 1 byte
+ buffer = make([]byte, 1)
+ n, err = syscall.Read(fd, buffer)
+ require.NoError(t, err)
+ require.Equal(t, 1, n)
+ require.Equal(t, []byte("A"), buffer)
+ })
+
+ t.Run("AppendMode", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "append_test.txt")
+
+ // Create file with initial content
+ err := os.WriteFile(testFile, []byte("initial"), 0644)
+ require.NoError(t, err)
+
+ // Open in append mode
+ fd, err := syscall.Open(testFile, syscall.O_WRONLY|syscall.O_APPEND, 0)
+ require.NoError(t, err)
+
+ // Write additional content
+ appendData := []byte(" appended")
+ n, err := syscall.Write(fd, appendData)
+ require.NoError(t, err)
+ require.Equal(t, len(appendData), n)
+
+ err = syscall.Close(fd)
+ require.NoError(t, err)
+
+ // Verify content
+ content, err := os.ReadFile(testFile)
+ require.NoError(t, err)
+ require.Equal(t, []byte("initial appended"), content)
+ })
+}
+
+// TestFileDescriptors tests POSIX file descriptor behavior
+func (s *POSIXComplianceTestSuite) TestFileDescriptors(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("DuplicateFileDescriptors", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "dup_test.txt")
+ testData := []byte("duplicate test")
+
+ // Create and open file
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Write initial data
+ n, err := syscall.Write(fd, testData)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+
+ // Duplicate file descriptor
+ dupFd, err := syscall.Dup(fd)
+ require.NoError(t, err)
+ defer syscall.Close(dupFd)
+
+ // Both descriptors should refer to the same file
+ // Seek on one should affect the other
+ pos, err := syscall.Seek(fd, 0, 0) // SEEK_SET
+ require.NoError(t, err)
+ require.Equal(t, int64(0), pos)
+
+ // Read from duplicate descriptor should start from position 0
+ buffer := make([]byte, 9)
+ n, err = syscall.Read(dupFd, buffer)
+ require.NoError(t, err)
+ require.Equal(t, 9, n)
+ require.Equal(t, []byte("duplicate"), buffer)
+ })
+
+ t.Run("FileDescriptorFlags", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "flags_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Open with close-on-exec flag
+ fd, err := syscall.Open(testFile, syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Verify close-on-exec flag is set
+ // Note: FcntlInt is not available on all platforms, this test may need platform-specific implementation
+ t.Skip("FcntlInt not available on this platform")
+ })
+}
+
+// TestAtomicOperations tests POSIX atomic operation compliance
+func (s *POSIXComplianceTestSuite) TestAtomicOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("AtomicRename", func(t *testing.T) {
+ oldFile := filepath.Join(mountPoint, "atomic_old.txt")
+ newFile := filepath.Join(mountPoint, "atomic_new.txt")
+ testData := []byte("atomic test data")
+
+ // Create source file
+ err := os.WriteFile(oldFile, testData, 0644)
+ require.NoError(t, err)
+
+ // Create existing target file
+ err = os.WriteFile(newFile, []byte("old content"), 0644)
+ require.NoError(t, err)
+
+ // Atomic rename should replace target
+ err = os.Rename(oldFile, newFile)
+ require.NoError(t, err)
+
+ // Verify source no longer exists
+ _, err = os.Stat(oldFile)
+ require.True(t, os.IsNotExist(err))
+
+ // Verify target has new content
+ content, err := os.ReadFile(newFile)
+ require.NoError(t, err)
+ require.Equal(t, testData, content)
+ })
+}
+
+// TestConcurrentAccess tests concurrent access patterns
+func (s *POSIXComplianceTestSuite) TestConcurrentAccess(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("ConcurrentReads", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "concurrent_read.txt")
+ testData := []byte("concurrent read test data")
+
+ // Create test file
+ err := os.WriteFile(testFile, testData, 0644)
+ require.NoError(t, err)
+
+ // Launch multiple concurrent readers
+ const numReaders = 10
+ results := make(chan error, numReaders)
+
+ for i := 0; i < numReaders; i++ {
+ go func() {
+ content, err := os.ReadFile(testFile)
+ if err != nil {
+ results <- err
+ return
+ }
+ if string(content) != string(testData) {
+ results <- fmt.Errorf("content mismatch")
+ return
+ }
+ results <- nil
+ }()
+ }
+
+ // Check all readers succeeded
+ for i := 0; i < numReaders; i++ {
+ err := <-results
+ require.NoError(t, err)
+ }
+ })
+
+ t.Run("ConcurrentWrites", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "concurrent_write.txt")
+
+ // Launch multiple concurrent writers
+ const numWriters = 5
+ results := make(chan error, numWriters)
+
+ for i := 0; i < numWriters; i++ {
+ go func(id int) {
+ content := fmt.Sprintf("writer %d data", id)
+ err := os.WriteFile(fmt.Sprintf("%s_%d", testFile, id), []byte(content), 0644)
+ results <- err
+ }(i)
+ }
+
+ // Check all writers succeeded
+ for i := 0; i < numWriters; i++ {
+ err := <-results
+ require.NoError(t, err)
+ }
+
+ // Verify all files were created
+ for i := 0; i < numWriters; i++ {
+ fileName := fmt.Sprintf("%s_%d", testFile, i)
+ _, err := os.Stat(fileName)
+ require.NoError(t, err)
+ }
+ })
+}
+
+// TestErrorHandling tests POSIX error handling compliance
+func (s *POSIXComplianceTestSuite) TestErrorHandling(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("AccessNonexistentFile", func(t *testing.T) {
+ nonexistentFile := filepath.Join(mountPoint, "does_not_exist.txt")
+
+ // Reading nonexistent file should return ENOENT
+ _, err := os.ReadFile(nonexistentFile)
+ require.Error(t, err)
+ require.True(t, os.IsNotExist(err))
+ })
+
+ t.Run("CreateFileInNonexistentDirectory", func(t *testing.T) {
+ fileInNonexistentDir := filepath.Join(mountPoint, "nonexistent_dir", "file.txt")
+
+ // Creating file in nonexistent directory should fail
+ err := os.WriteFile(fileInNonexistentDir, []byte("test"), 0644)
+ require.Error(t, err)
+ require.True(t, os.IsNotExist(err))
+ })
+
+ t.Run("InvalidFileDescriptor", func(t *testing.T) {
+ // Using an invalid file descriptor should return appropriate error
+ buffer := make([]byte, 10)
+ _, err := syscall.Read(999, buffer) // 999 is likely an invalid fd
+ require.Error(t, err)
+ require.Equal(t, syscall.EBADF, err)
+ })
+
+ t.Run("PermissionDenied", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "readonly.txt")
+
+ // Create read-only file
+ err := os.WriteFile(testFile, []byte("readonly"), 0444)
+ require.NoError(t, err)
+
+ // Attempting to write to read-only file should fail
+ err = os.WriteFile(testFile, []byte("modified"), 0444)
+ require.Error(t, err)
+ require.True(t, os.IsPermission(err))
+ })
+}