diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 775ccc54..066c32c5 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: go-version: [1.22, 1.23] - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go diff --git a/CHANGELOG.md b/CHANGELOG.md index e86486e9..096ee992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Windows support in the os backend. +### Fixed +- Ability to run all unit tests on Windows. ## [6.22.0] - 2024-11-06 ### Fixed diff --git a/backend/azure/fileSystem.go b/backend/azure/fileSystem.go index 23280208..87bcb189 100644 --- a/backend/azure/fileSystem.go +++ b/backend/azure/fileSystem.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/url" - "os" "path" "regexp" "strings" @@ -133,11 +132,8 @@ func ParsePath(p string) (host, pth string, err error) { if p == "/" { return "", "", errors.New("no container specified for Azure path") } - var isLocation bool - if p[len(p)-1:] == string(os.PathSeparator) { - isLocation = true - } - l := strings.Split(p, string(os.PathSeparator)) + isLocation := strings.HasSuffix(p, "/") + l := strings.Split(p, "/") p = utils.EnsureLeadingSlash(path.Join(l[2:]...)) if isLocation { p = utils.EnsureTrailingSlash(p) diff --git a/backend/mem/file_test.go b/backend/mem/file_test.go index dc3ee91f..b9cfd1c5 100644 --- a/backend/mem/file_test.go +++ b/backend/mem/file_test.go @@ -37,10 +37,10 @@ func (s *memFileTest) SetupTest() { s.NoError(s.testFile.Touch(), "unexpected error touching file") } -func (s *memFileTest) TeardownTest() { +func (s *memFileTest) TearDownTest() { err := s.testFile.Close() s.NoError(err, "close error not expected") - s.NoError(s.testFile.Delete(), "delete failed unexpectedly") + _ = s.testFile.Delete() } // TestZBR ensures that we can always read zero bytes @@ -382,7 +382,7 @@ func (s *memFileTest) TestCopyToLocationOS() { s.NotNil(copiedFile) s.Equal("/test_files/test.txt", s.testFile.Path()) // testFile's path should be unchanged - s.Equal(filepath.Join(dir, "test.txt"), copiedFile.Path()) // new path should be that + s.Equal(path.Join(osFile.Location().Path(), "test.txt"), copiedFile.Path()) // new path should be that _, err = copiedFile.Read(readSlice) s.NoError(err, "unexpected read error") @@ -390,6 +390,7 @@ func (s *memFileTest) TestCopyToLocationOS() { _, err = s.testFile.Read(readSlice2) s.NoError(err, "unexpected read error") s.Equal(readSlice2, readSlice) // both reads should be the same + s.Require().NoError(copiedFile.Close()) cleanErr := os.RemoveAll(dir) // clean up s.NoError(cleanErr, "unexpected error cleaning up osFiles") } diff --git a/backend/os/file.go b/backend/os/file.go index 91985b93..6d7b2fc5 100644 --- a/backend/os/file.go +++ b/backend/os/file.go @@ -7,6 +7,7 @@ import ( "os" "path" "path/filepath" + "runtime" "time" "github.com/c2fo/vfs/v6" @@ -22,6 +23,7 @@ type opener func(filePath string) (*os.File, error) // File implements vfs.File interface for os fs. type File struct { file *os.File + volume string name string filesystem *FileSystem cursorPos int64 @@ -34,7 +36,7 @@ type File struct { // Delete unlinks the file returning any error or nil. func (f *File) Delete(_ ...options.DeleteOption) error { - err := os.Remove(f.Path()) + err := os.Remove(osFilePath(f)) if err == nil { f.file = nil } @@ -43,7 +45,7 @@ func (f *File) Delete(_ ...options.DeleteOption) error { // LastModified returns the timestamp of the file's mtime or error, if any. func (f *File) LastModified() (*time.Time, error) { - stats, err := os.Stat(f.Path()) + stats, err := os.Stat(osFilePath(f)) if err != nil { return nil, err } @@ -66,12 +68,12 @@ func (f *File) Name() string { // // someFile.Location().Path() func (f *File) Path() string { - return filepath.Join(f.Location().Path(), f.Name()) + return path.Join(f.Location().Path(), f.Name()) } // Size returns the size (in bytes) of the File or any error. func (f *File) Size() (uint64, error) { - stats, err := os.Stat(f.Path()) + stats, err := os.Stat(osFilePath(f)) if err != nil { return 0, err } @@ -181,7 +183,7 @@ func (f *File) Seek(offset int64, whence int) (int64, error) { // Exists true if the file exists on the file system, otherwise false, and an error, if any. func (f *File) Exists() (bool, error) { - _, err := os.Stat(f.Path()) + _, err := os.Stat(osFilePath(f)) if err != nil { // file does not exist if os.IsNotExist(err) { @@ -217,19 +219,22 @@ func (f *File) Write(p []byte) (n int, err error) { func (f *File) Location() vfs.Location { return &Location{ fileSystem: f.filesystem, + volume: f.volume, name: utils.EnsureTrailingSlash(path.Dir(f.name)), } } // MoveToFile move a file. It accepts a target vfs.File and returns an error, if any. func (f *File) MoveToFile(file vfs.File) error { - // validate seek is at 0,0 before doing copy - if err := backend.ValidateCopySeekPosition(f); err != nil { - return err + if f.file != nil { + // validate seek is at 0,0 before doing copy + if err := backend.ValidateCopySeekPosition(f); err != nil { + return err + } } // handle native os move/rename if file.Location().FileSystem().Scheme() == Scheme { - return safeOsRename(f.Path(), file.Path()) + return safeOsRename(osFilePath(f), osFilePath(file)) } // do copy/delete move for non-native os moves @@ -245,7 +250,7 @@ func safeOsRename(srcName, dstName string) error { err := os.Rename(srcName, dstName) if err != nil { e, ok := err.(*os.LinkError) - if ok && e.Err.Error() == osCrossDeviceLinkError { + if ok && (e.Err.Error() == osCrossDeviceLinkError || (runtime.GOOS == "windows" && e.Err.Error() == "Access is denied.")) { // do cross-device renaming if err := osCopy(srcName, dstName); err != nil { return err @@ -306,9 +311,11 @@ func (f *File) MoveToLocation(location vfs.Location) (vfs.File, error) { // CopyToFile copies the file to a new File. It accepts a vfs.File and returns an error, if any. func (f *File) CopyToFile(file vfs.File) error { - // validate seek is at 0,0 before doing copy - if err := backend.ValidateCopySeekPosition(f); err != nil { - return err + if f.file != nil { + // validate seek is at 0,0 before doing copy + if err := backend.ValidateCopySeekPosition(f); err != nil { + return err + } } _, err := f.copyWithName(file.Name(), file.Location()) return err @@ -317,9 +324,11 @@ func (f *File) CopyToFile(file vfs.File) error { // CopyToLocation copies existing File to new Location with the same name. // It accepts a vfs.Location and returns a vfs.File and error, if any. func (f *File) CopyToLocation(location vfs.Location) (vfs.File, error) { - // validate seek is at 0,0 before doing copy - if err := backend.ValidateCopySeekPosition(f); err != nil { - return nil, err + if f.file != nil { + // validate seek is at 0,0 before doing copy + if err := backend.ValidateCopySeekPosition(f); err != nil { + return nil, err + } } return f.copyWithName(f.Name(), location) } @@ -351,7 +360,7 @@ func (f *File) Touch() error { return f.Close() } now := time.Now() - return os.Chtimes(f.Path(), now, now) + return os.Chtimes(osFilePath(f), now, now) } func (f *File) copyWithName(name string, location vfs.Location) (vfs.File, error) { @@ -386,7 +395,7 @@ func (f *File) openFile() (*os.File, error) { openFunc = f.fileOpener } - file, err := openFunc(f.Path()) + file, err := openFunc(osFilePath(f)) if err != nil { return nil, err } @@ -398,7 +407,7 @@ func (f *File) openFile() (*os.File, error) { func openOSFile(filePath string) (*os.File, error) { // Ensure the path exists before opening the file, NoOp if dir already exists. var fileMode os.FileMode = 0666 - if err := os.MkdirAll(path.Dir(filePath), os.ModeDir|0750); err != nil { + if err := os.MkdirAll(filepath.Dir(filePath), os.ModeDir|0750); err != nil { return nil, err } @@ -431,7 +440,7 @@ func (f *File) getInternalFile() (*os.File, error) { openFunc = f.fileOpener } - finalFile, err := openFunc(f.Path()) + finalFile, err := openFunc(osFilePath(f)) if err != nil { return nil, err } @@ -475,10 +484,11 @@ func (f *File) copyToLocalTempReader() (*os.File, error) { openFunc = f.fileOpener } - actualFile, err := openFunc(f.Path()) + actualFile, err := openFunc(osFilePath(f)) if err != nil { return nil, err } + defer func() { _ = actualFile.Close() }() if _, err := io.Copy(tmpFile, actualFile); err != nil { return nil, err } @@ -493,3 +503,10 @@ func (f *File) copyToLocalTempReader() (*os.File, error) { return tmpFile, nil } + +func osFilePath(f vfs.File) string { + if runtime.GOOS == "windows" { + return f.Location().Volume() + filepath.FromSlash(f.Path()) + } + return f.Path() +} diff --git a/backend/os/fileSystem.go b/backend/os/fileSystem.go index 32e819d1..07ca8c05 100644 --- a/backend/os/fileSystem.go +++ b/backend/os/fileSystem.go @@ -2,6 +2,8 @@ package os import ( "path" + "path/filepath" + "runtime" "github.com/c2fo/vfs/v6" "github.com/c2fo/vfs/v6/backend" @@ -22,15 +24,31 @@ func (fs *FileSystem) Retry() vfs.Retry { // NewFile function returns the os implementation of vfs.File. func (fs *FileSystem) NewFile(volume, name string) (vfs.File, error) { + if runtime.GOOS == "windows" && filepath.IsAbs(name) { + if v := filepath.VolumeName(name); v != "" { + volume = v + name = name[len(v):] + } + } + + name = filepath.ToSlash(name) err := utils.ValidateAbsoluteFilePath(name) if err != nil { return nil, err } - return &File{name: name, filesystem: fs}, nil + return &File{volume: volume, name: name, filesystem: fs}, nil } // NewLocation function returns the os implementation of vfs.Location. func (fs *FileSystem) NewLocation(volume, name string) (vfs.Location, error) { + if runtime.GOOS == "windows" && filepath.IsAbs(name) { + if v := filepath.VolumeName(name); v != "" { + volume = v + name = name[len(v):] + } + } + + name = filepath.ToSlash(name) err := utils.ValidateAbsoluteLocationPath(name) if err != nil { return nil, err @@ -38,6 +56,7 @@ func (fs *FileSystem) NewLocation(volume, name string) (vfs.Location, error) { return &Location{ fileSystem: fs, + volume: volume, name: utils.EnsureTrailingSlash(path.Clean(name)), }, nil } diff --git a/backend/os/file_test.go b/backend/os/file_test.go index 2b2eb8fb..d39bf9b9 100644 --- a/backend/os/file_test.go +++ b/backend/os/file_test.go @@ -6,7 +6,9 @@ import ( "io" "os" "path" + "path/filepath" "testing" + "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" @@ -48,7 +50,7 @@ func (s *osFileTest) SetupTest() { s.testFile = file } -func (s *osFileTest) TeardownTest() { +func (s *osFileTest) TearDownTest() { err := s.testFile.Close() s.NoError(err, "close error not expected") } @@ -93,6 +95,8 @@ func (s *osFileTest) TestTouch() { firstModTime, err := testfile.LastModified() s.NoError(err) + time.Sleep(time.Millisecond) + // touch again err = testfile.Touch() s.NoError(err) @@ -173,14 +177,6 @@ func (s *osFileTest) TestSeek() { s.NoError(rerr, "read error not expected") s.Equal(expectedText, string(data)) s.NoError(s.testFile.Close()) - - // setup file for err out of opening - f, err := s.tmploc.NewFile("test_files/seekFileFail.txt") - s.NoError(err) - f.(*File).useTempFile = true - f.(*File).fileOpener = func(filePath string) (*os.File, error) { return nil, errors.New("bad opener") } - _, err = f.Seek(0, 4) - s.Error(err) } func (s *osFileTest) TestCopyToLocation() { @@ -193,7 +189,7 @@ func (s *osFileTest) TestCopyToLocation() { otherFile.On("Close").Return(nil) otherFs.On("NewFile", mock.Anything, mock.Anything).Return(otherFile, nil) - location := Location{"/some/path", otherFs} + location := Location{name: "/some/path", fileSystem: otherFs} _, err := s.testFile.CopyToLocation(&location) s.NoError(err) @@ -208,7 +204,7 @@ func (s *osFileTest) TestCopyToFile() { otherFs := &mocks.FileSystem{} otherFile := new(mocks.File) - location := Location{"/some/path", otherFs} + location := Location{name: "/some/path", fileSystem: otherFs} // Expected behavior otherFile.On("Write", mock.Anything).Return(len(expectedText), nil) @@ -231,7 +227,7 @@ func (s *osFileTest) TestEmptyCopyToFile() { otherFs := new(mocks.FileSystem) otherFile := new(mocks.File) - location := Location{"/some/path", otherFs} + location := Location{name: "/some/path", fileSystem: otherFs} // Expected behavior otherFile.On("Write", mock.Anything).Return(len(expectedText), nil) @@ -263,7 +259,7 @@ func (s *osFileTest) TestCopyToLocationIgnoreExtraSeparator() { otherFs.On("NewFile", mock.Anything, mock.Anything).Return(otherFile, nil) // Add trailing slash - location := Location{"/some/path/", otherFs} + location := Location{name: "/some/path/", fileSystem: otherFs} _, err := s.testFile.CopyToLocation(&location) s.Require().NoError(err) @@ -273,10 +269,10 @@ func (s *osFileTest) TestCopyToLocationIgnoreExtraSeparator() { func (s *osFileTest) TestMoveToLocation() { expectedText := "moved file" - dir, terr := os.MkdirTemp(path.Join(s.tmploc.Path(), "test_files"), "example") + dir, terr := os.MkdirTemp(filepath.Join(osLocationPath(s.tmploc), "test_files"), "example") s.NoError(terr) - origFileName := path.Join(dir, "test_files/move.txt") + origFileName := filepath.Join(dir, "test_files", "move.txt") file, nerr := s.fileSystem.NewFile("", origFileName) s.NoError(nerr) @@ -296,10 +292,11 @@ func (s *osFileTest) TestMoveToLocation() { s.True(found) // setup location - location := Location{dir, s.fileSystem} + location, err := s.fileSystem.NewLocation("", utils.EnsureTrailingSlash(dir)) + s.Require().NoError(err) // move the file to new location - movedFile, err := file.MoveToLocation(&location) + movedFile, err := file.MoveToLocation(location) s.NoError(err) s.Equal(location.Path(), movedFile.Location().Path(), "ensure file location changed") @@ -336,7 +333,7 @@ func (s *osFileTest) TestMoveToLocation() { } func (s *osFileTest) TestSafeOsRename() { - dir, err := os.MkdirTemp(path.Join(s.tmploc.Path(), "test_files"), "example") + dir, err := os.MkdirTemp(filepath.Join(osLocationPath(s.tmploc), "test_files"), "example") s.NoError(err) defer func() { err := os.RemoveAll(dir) @@ -377,7 +374,7 @@ func (s *osFileTest) TestSafeOsRename() { } func (s *osFileTest) TestOsCopy() { - dir, err := os.MkdirTemp(path.Join(s.tmploc.Path(), "test_files"), "example") + dir, err := os.MkdirTemp(filepath.Join(osLocationPath(s.tmploc), "test_files"), "example") s.NoError(err) defer func() { err := os.RemoveAll(dir) @@ -399,10 +396,12 @@ func (s *osFileTest) TestOsCopy() { b, err := io.ReadAll(file2) s.NoError(err) s.Equal(testBytes, b, "contents match") + + s.Require().NoError(file2.Close()) } func (s *osFileTest) TestMoveToFile() { - dir, terr := os.MkdirTemp(path.Join(s.tmploc.Path(), "test_files"), "example") + dir, terr := os.MkdirTemp(filepath.Join(osLocationPath(s.tmploc), "test_files"), "example") s.NoError(terr) file1, err := s.fileSystem.NewFile("", path.Join(dir, "original.txt")) @@ -509,6 +508,7 @@ func (s *osFileTest) TestWrite() { data = make([]byte, 4) _, err = f.Write(data) s.Error(err) + s.Require().NoError(f.Close()) } func (s *osFileTest) TestCursor() { @@ -638,7 +638,7 @@ func (s *osFileTest) TestLastModified() { lastModified, err := file.LastModified() s.NoError(err) - osStats, err := os.Stat(path.Join(s.tmploc.Path(), "test_files/test.txt")) + osStats, err := os.Stat(filepath.Join(osLocationPath(s.tmploc), "test_files", "test.txt")) s.NoError(err) s.NotNil(lastModified) s.Equal(osStats.ModTime(), *lastModified) @@ -663,7 +663,7 @@ func (s *osFileTest) TestSize() { size, err := file.Size() s.NoError(err) - osStats, err := os.Stat(path.Join(s.tmploc.Path(), "test_files/test.txt")) + osStats, err := os.Stat(filepath.Join(osLocationPath(s.tmploc), "test_files", "test.txt")) s.NoError(err) s.NotNil(size) s.Equal(osStats.Size(), int64(size)) @@ -684,14 +684,14 @@ func (s *osFileTest) TestPath() { func (s *osFileTest) TestURI() { file, err := s.tmploc.NewFile("some/file/test.txt") s.NoError(err) - expected := fmt.Sprintf("file://%s", path.Join(s.tmploc.Path(), "some/file/test.txt")) + expected := fmt.Sprintf("file://%s", filepath.ToSlash(filepath.Join(osLocationPath(s.tmploc), "some", "file", "test.txt"))) s.Equal(expected, file.URI(), "%s does not match %s", file.URI(), expected) } func (s *osFileTest) TestStringer() { file, err := s.tmploc.NewFile("some/file/test.txt") s.NoError(err) - s.Equal(fmt.Sprintf("file://%s", path.Join(s.tmploc.Path(), "some/file/test.txt")), file.String()) + s.Equal(fmt.Sprintf("file://%s", filepath.ToSlash(filepath.Join(osLocationPath(s.tmploc), "some", "file", "test.txt"))), file.String()) } func (s *osFileTest) TestLocationRightAfterChangeDir() { diff --git a/backend/os/location.go b/backend/os/location.go index 764392c7..158e6b78 100644 --- a/backend/os/location.go +++ b/backend/os/location.go @@ -6,6 +6,7 @@ import ( "path" "path/filepath" "regexp" + "runtime" "strings" "github.com/c2fo/vfs/v6" @@ -15,6 +16,7 @@ import ( // Location implements the vfs.Location interface specific to OS fs. type Location struct { + volume string name string fileSystem vfs.FileSystem } @@ -28,6 +30,7 @@ func (l *Location) NewFile(fileName string) (vfs.File, error) { if fileName == "" { return nil, errors.New("non-empty string filePath is required") } + fileName = filepath.ToSlash(fileName) err := utils.ValidateRelativeFilePath(fileName) if err != nil { return nil, err @@ -112,7 +115,7 @@ func (l *Location) fileList(testEval fileTest) ([]string, error) { // Volume returns the volume, if any, of the location. Given "C:\foo\bar" it returns "C:" on Windows. On other platforms it returns "". func (l *Location) Volume() string { - return filepath.VolumeName(l.name) + return l.volume } // Path returns the location path. @@ -124,7 +127,7 @@ func (l *Location) Path() string { // permissions. Will receive false without an error if the location simply doesn't exist. Otherwise could receive // false and any errors passed back from the OS. func (l *Location) Exists() (bool, error) { - _, err := os.Stat(l.Path()) + _, err := os.Stat(osLocationPath(l)) if err != nil { if os.IsNotExist(err) { return false, nil @@ -155,6 +158,7 @@ func (l *Location) NewLocation(relativePath string) (vfs.Location, error) { // make a copy of the original location first, then ChangeDir, leaving the original location as-is newLocation := &Location{} *newLocation = *l + relativePath = filepath.ToSlash(relativePath) err := newLocation.ChangeDir(relativePath) if err != nil { return nil, err @@ -186,3 +190,10 @@ func (l *Location) ChangeDir(relativePath string) error { func (l *Location) FileSystem() vfs.FileSystem { return l.fileSystem } + +func osLocationPath(l vfs.Location) string { + if runtime.GOOS == "windows" { + return l.Volume() + filepath.FromSlash(l.Path()) + } + return l.Path() +} diff --git a/backend/os/location_test.go b/backend/os/location_test.go index 7993f2d5..243da499 100644 --- a/backend/os/location_test.go +++ b/backend/os/location_test.go @@ -2,6 +2,7 @@ package os import ( "os" + "path" "path/filepath" "regexp" "testing" @@ -116,14 +117,14 @@ func (s *osLocationTest) TestChangeDir() { cwd := fileLocation.Path() err := fileLocation.ChangeDir("other/") s.NoError(err, "change dir error not expected") - s.Equal(fileLocation.Path(), utils.EnsureTrailingSlash(filepath.Join(cwd, "other"))) + s.Equal(fileLocation.Path(), utils.EnsureTrailingSlash(path.Join(cwd, "other"))) } func (s *osLocationTest) TestVolume() { volume := s.testFile.Location().Volume() // For Unix, this returns an empty string. For windows, it would be something like 'C:' - s.Equal(filepath.VolumeName("test_files/test.txt"), volume) + s.Equal(filepath.VolumeName(os.TempDir()), volume) } func (s *osLocationTest) TestPath() { diff --git a/backend/sftp/options.go b/backend/sftp/options.go index c517ad7b..a42e0e2e 100644 --- a/backend/sftp/options.go +++ b/backend/sftp/options.go @@ -5,7 +5,7 @@ import ( "fmt" "io" "os" - "path" + "path/filepath" "runtime" "strconv" @@ -231,7 +231,10 @@ func findHomeSystemKnownHosts(knownHostsFiles []string) ([]string, error) { if err != nil { return nil, err } - homeKnownHostsPath := utils.EnsureLeadingSlash(path.Join(home, ".ssh/known_hosts")) + homeKnownHostsPath := filepath.Join(home, ".ssh", "known_hosts") + if runtime.GOOS != "windows" { + homeKnownHostsPath = utils.EnsureLeadingSlash(homeKnownHostsPath) + } // check file existence first to prevent auto-vivification of file found, err := foundFile(homeKnownHostsPath) diff --git a/backend/sftp/options_test.go b/backend/sftp/options_test.go index 72790c93..e9f4defa 100644 --- a/backend/sftp/options_test.go +++ b/backend/sftp/options_test.go @@ -103,6 +103,7 @@ type getFileTest struct { keyfile string passphrase string hasError bool + err error errMessage string message string } @@ -127,7 +128,7 @@ func (o *optionsSuite) TestGetKeyFile() { keyfile: "nonexistent.key", passphrase: "", hasError: true, - errMessage: "open nonexistent.key: no such file or directory", + err: os.ErrNotExist, message: "file not found", }, { @@ -153,7 +154,11 @@ func (o *optionsSuite) TestGetKeyFile() { o.Run(t.message, func() { _, err := getKeyFile(t.keyfile, t.passphrase) if t.hasError { - o.EqualError(err, t.errMessage, t.message) + if t.err != nil { + o.ErrorIs(err, t.err, t.message) + } else { + o.EqualError(err, t.errMessage, t.message) + } } else { o.NoError(err, t.message) } @@ -269,6 +274,7 @@ type authTest struct { returnCount int hasError bool errMessage string + err error message string } @@ -374,7 +380,7 @@ func (o *optionsSuite) TestGetAuthMethods() { }, returnCount: 1, hasError: true, - errMessage: "open nonexistent.key: no such file or directory", + err: os.ErrNotExist, message: "env var keyfile returns error for file not found", }, } @@ -391,7 +397,11 @@ func (o *optionsSuite) TestGetAuthMethods() { // apply test auth, err := getAuthMethods(t.options) if t.hasError { - o.EqualError(err, t.errMessage, t.message) + if t.err != nil { + o.ErrorIs(err, t.err, t.message) + } else { + o.EqualError(err, t.errMessage, t.message) + } } else { o.NoError(err, t.message) o.Len(auth, t.returnCount, "auth count") @@ -409,6 +419,7 @@ type getClientTest struct { options Options authority utils.Authority hasError bool + err error errRegex string message string } @@ -435,7 +446,7 @@ func (o *optionsSuite) TestGetClient() { KnownHostsCallback: ssh.FixedHostKey(o.publicKey), }, hasError: true, - errRegex: "open nonexistent.key: no such file or directory", + err: os.ErrNotExist, message: "getclient - bad auth key", }, { @@ -455,8 +466,12 @@ func (o *optionsSuite) TestGetClient() { _, _, err := getClient(t.authority, t.options) if t.hasError { if o.Error(err, "error found") { - re := regexp.MustCompile(t.errRegex) - o.Regexp(re, err.Error(), "error matches") + if t.err != nil { + o.ErrorIs(err, t.err, t.message) + } else { + re := regexp.MustCompile(t.errRegex) + o.Regexp(re, err.Error(), "error matches") + } } } else { o.NoError(err, t.message) diff --git a/utils/utils.go b/utils/utils.go index c547f9ea..b3eb579d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -7,6 +7,7 @@ import ( "net/url" "path/filepath" "regexp" + "runtime" "strings" "time" @@ -137,12 +138,20 @@ func PathToURI(p string) (string, error) { return p, nil } - // make absolute path (if not already) - absPath, err := filepath.Abs(p) - if err != nil { - return "", err + absPath := p + if p[0] != '/' { + // make absolute path (if not already) + absPath, err = filepath.Abs(p) + if err != nil { + return "", err + } + if runtime.GOOS == "windows" { + absPath = "/" + absPath + } } + absPath = filepath.ToSlash(absPath) + // Abs() strips trailing slashes so add back if original path had slash if p[len(p)-1:] == "/" { absPath = EnsureTrailingSlash(absPath) diff --git a/utils/utils_test.go b/utils/utils_test.go index af38f8a9..0d2223b4 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -715,6 +715,8 @@ func (s *utilsSuite) TestTouchCopy() { s.NoError(err, "file should exist, so no error") s.NotZero(fi, "file should have a non-zero byte size") + s.NoError(reader.Close()) + // TouchCopy should fail on a reader.Size() error nonexistentFile := path.Join(writer.Path(), "nonexistent.file") noFile, err := osfs.NewFile("", nonexistentFile) @@ -790,6 +792,8 @@ func (s *utilsSuite) TestTouchCopyBufferedDefaultBufferSize() { s.NoError(err, "file should exist, so no error") s.NotZero(fi, "file should have a non-zero byte size") + s.NoError(reader.Close()) + // TouchCopyBuffered should fail on a reader.Size() error nonexistentFile := path.Join(writer.Path(), "nonexistent.file") noFile, err := osfs.NewFile("", nonexistentFile) @@ -865,6 +869,8 @@ func (s *utilsSuite) TestTouchCopyBufferedNonDefaultBufferSize() { s.NoError(err, "file should exist, so no error") s.NotZero(fi, "file should have a non-zero byte size") + s.NoError(reader.Close()) + // TouchCopyBuffered should fail on a reader.Size() error nonexistentFile := path.Join(writer.Path(), "nonexistent.file") noFile, err := osfs.NewFile("", nonexistentFile)