diff options
author | Jeremy Kuhne <jeremy.kuhne@microsoft.com> | 2017-05-19 23:20:11 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-19 23:20:11 +0300 |
commit | 801dde95a5eac06140d0ac633ac3f9bfdd25aca5 (patch) | |
tree | f5bedd525be96f65ab99472a5546155c3399f92c /src | |
parent | 5350d4e1321d84df1e377060710fae8f58ca0149 (diff) |
Fix Unix missing file state (#19959)
* Fix Unix missing file state
In the original .NET implementation a missing file never
throws directly with FileInfo. Attributes get the error result
from Windows (-1) and times aren't initialized.
* Feedback
* Change FileSystemTest derivation
FileSystemTest should derive from RemoteFileSystemTest so
we can have one base class.
* Add RemoteExecutorBase to performance test project
* Address test feedback
* Fix File.GetAttributes on Unix
NetFX doesn't rely on FileInfo and throws when getting attributes
on missing files through File.
Diffstat (limited to 'src')
20 files changed, 326 insertions, 105 deletions
diff --git a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs index 28db0fbe09..647bcd3c82 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs @@ -12,6 +12,11 @@ namespace System.IO private bool _targetOfSymlinkIsDirectory; /// <summary> + /// Exists as a path as of last refresh. + /// </summary> + private bool _exists; + + /// <summary> /// Whether we've successfully cached a stat structure. /// -1 if we need to refresh _fileStatus, 0 if we've successfully cached one, /// or any other value that serves as an errno error code from the @@ -35,6 +40,9 @@ namespace System.IO { EnsureStatInitialized(); + if (!_exists) + return (FileAttributes)(-1); + FileAttributes attrs = default(FileAttributes); if (IsDirectoryAssumesInitialized) // this is the one attribute where we follow symlinks @@ -85,11 +93,31 @@ namespace System.IO // The only thing we can reasonably change is whether the file object is readonly, // just changing its permissions accordingly. EnsureStatInitialized(); + + if (!_exists) + { + ThrowNotFound(FullPath); + } + IsReadOnlyAssumesInitialized = (value & FileAttributes.ReadOnly) != 0; _fileStatusInitialized = -1; } } + internal static void ThrowNotFound(string path) + { + // Windows distinguishes between whether the directory or the file isn't found, + // and throws a different exception in these cases. We attempt to approximate that + // here; there is a race condition here, where something could change between + // when the error occurs and our checks, but it's the best we can do, and the + // worst case in such a race condition (which could occur if the file system is + // being manipulated concurrently with these checks) is that we throw a + // FileNotFoundException instead of DirectoryNotFoundException. + + bool directoryError = !Directory.Exists(Path.GetDirectoryName(PathHelpers.TrimEndingDirectorySeparator(path))); + throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(Interop.Error.ENOENT), path, directoryError); + } + /// <summary>Gets whether stat reported this system object as a directory.</summary> private bool IsDirectoryAssumesInitialized => (_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR || @@ -161,7 +189,7 @@ namespace System.IO } return - _fileStatusInitialized == 0 && // avoid throwing if Refresh failed; instead just return false + _exists && (this is DirectoryInfo) == IsDirectoryAssumesInitialized; } } @@ -171,6 +199,9 @@ namespace System.IO get { EnsureStatInitialized(); + if (!_exists) + return DateTimeOffset.FromFileTime(0); + long rawTime = (_fileStatus.Flags & Interop.Sys.FileStatusFlags.HasBirthTime) != 0 ? _fileStatus.BirthTime : Math.Min(_fileStatus.CTime, _fileStatus.MTime); // fall back to the oldest time we have in between change and modify time @@ -190,6 +221,8 @@ namespace System.IO get { EnsureStatInitialized(); + if (!_exists) + return DateTimeOffset.FromFileTime(0); return DateTimeOffset.FromUnixTimeSeconds(_fileStatus.ATime).ToLocalTime(); } set { SetAccessWriteTimes(value.ToUnixTimeSeconds(), null); } @@ -200,6 +233,8 @@ namespace System.IO get { EnsureStatInitialized(); + if (!_exists) + return DateTimeOffset.FromFileTime(0); return DateTimeOffset.FromUnixTimeSeconds(_fileStatus.MTime).ToLocalTime(); } set { SetAccessWriteTimes(null, value.ToUnixTimeSeconds()); } @@ -236,17 +271,32 @@ namespace System.IO // storing those results separately. We only report failure if the initial // lstat fails, as a broken symlink should still report info on exists, attributes, etc. _targetOfSymlinkIsDirectory = false; - int result = Interop.Sys.LStat(FullPath, out _fileStatus); + string path = PathHelpers.TrimEndingDirectorySeparator(FullPath); + int result = Interop.Sys.LStat(path, out _fileStatus); if (result < 0) { Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); - _fileStatusInitialized = errorInfo.RawErrno; + + // This should never set the error if the file can't be found. + // (see the Windows refresh passing returnErrorOnNotFound: false). + if (errorInfo.Error == Interop.Error.ENOENT + || errorInfo.Error == Interop.Error.ENOTDIR) + { + _fileStatusInitialized = 0; + _exists = false; + } + else + { + _fileStatusInitialized = errorInfo.RawErrno; + } return; } + _exists = true; + Interop.Sys.FileStatus targetStatus; if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK && - Interop.Sys.Stat(FullPath, out targetStatus) >= 0) + Interop.Sys.Stat(path, out targetStatus) >= 0) { _targetOfSymlinkIsDirectory = (targetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; } @@ -265,20 +315,7 @@ namespace System.IO { int errno = _fileStatusInitialized; _fileStatusInitialized = -1; - var errorInfo = new Interop.ErrorInfo(errno); - - // Windows distinguishes between whether the directory or the file isn't found, - // and throws a different exception in these cases. We attempt to approximate that - // here; there is a race condition here, where something could change between - // when the error occurs and our checks, but it's the best we can do, and the - // worst case in such a race condition (which could occur if the file system is - // being manipulated concurrently with these checks) is that we throw a - // FileNotFoundException instead of DirectoryNotFoundexception. - - // directoryError is true only if a FileNotExists error was provided and the parent - // directory of the file represented by _fullPath is nonexistent - bool directoryError = (errorInfo.Error == Interop.Error.ENOENT && !Directory.Exists(Path.GetDirectoryName(PathHelpers.TrimEndingDirectorySeparator(FullPath)))); // The destFile's path is invalid - throw Interop.GetExceptionForIoErrno(errorInfo, FullPath, directoryError); + throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), FullPath); } } } diff --git a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs index 46aacb6420..3c6aa499c5 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs @@ -106,7 +106,7 @@ namespace System.IO get { EnsureDataInitialized(); - return ((long)_data.fileSizeHigh) << 32 | ((long)_data.fileSizeLow & 0xFFFFFFFFL); + return ((long)_data.fileSizeHigh) << 32 | _data.fileSizeLow & 0xFFFFFFFFL; } } @@ -126,7 +126,7 @@ namespace System.IO { // This should not throw, instead we store the result so that we can throw it // when someone actually accesses a property - _dataInitialized = Win32FileSystem.FillAttributeInfo(FullPath, ref _data, false, false); + _dataInitialized = Win32FileSystem.FillAttributeInfo(FullPath, ref _data, tryagain: false, returnErrorOnNotFound: false); } } } diff --git a/src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs b/src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs index 9d2ef672bc..1d25e9c3b0 100644 --- a/src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs +++ b/src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs @@ -672,7 +672,12 @@ namespace System.IO public override FileAttributes GetAttributes(string fullPath) { - return new FileInfo(fullPath, null).Attributes; + FileAttributes attributes = new FileInfo(fullPath, null).Attributes; + + if (attributes == (FileAttributes)(-1)) + FileSystemInfo.ThrowNotFound(fullPath); + + return attributes; } public override void SetAttributes(string fullPath, FileAttributes attributes) diff --git a/src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs b/src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs index 11c27349d8..a2a6733554 100644 --- a/src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs +++ b/src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs @@ -234,10 +234,13 @@ namespace System.IO // Remove trailing slash since this can cause grief to FindFirstFile. You will get an invalid argument error String tempPath = path.TrimEnd(PathHelpers.DirectorySeparatorChars); - // For floppy drives, normally the OS will pop up a dialog saying - // there is no disk in drive A:, please insert one. We don't want that. - // SetErrorMode will let us disable this, but we should set the error - // mode back, since this may have wide-ranging effects. + // For removable media drives, normally the OS will pop up a dialog requesting insertion + // of the relevant media (CD, floppy, memory card, etc.). We don't want this prompt so we + // set SEM_FAILCRITICALERRORS to suppress it. + // + // Note that said dialog only shows once the relevant filesystem has been loaded, which + // does not happen until actual media is accessed at least once since booting. + uint oldMode; bool success = Interop.Kernel32.SetThreadErrorMode(Interop.Kernel32.SEM_FAILCRITICALERRORS, out oldMode); try @@ -253,7 +256,7 @@ namespace System.IO if (errorCode == Interop.Errors.ERROR_FILE_NOT_FOUND || errorCode == Interop.Errors.ERROR_PATH_NOT_FOUND || - errorCode == Interop.Errors.ERROR_NOT_READY) // floppy device not ready + errorCode == Interop.Errors.ERROR_NOT_READY) // Removable media not inserted { if (!returnErrorOnNotFound) { diff --git a/src/System.IO.FileSystem/tests/Directory/Delete.cs b/src/System.IO.FileSystem/tests/Directory/Delete.cs index b88478abbb..23721d88e9 100644 --- a/src/System.IO.FileSystem/tests/Directory/Delete.cs +++ b/src/System.IO.FileSystem/tests/Directory/Delete.cs @@ -75,10 +75,18 @@ namespace System.IO.Tests Assert.False(testDir.Exists); } - [Fact] - public void ShouldThrowDirectoryNotFoundExceptionForNonexistentDirectory() + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingFile_ThrowsDirectoryNotFound(char trailingChar) + { + string path = GetTestFilePath() + trailingChar; + Assert.Throws<DirectoryNotFoundException>(() => Delete(path)); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingDirectory_ThrowsDirectoryNotFound(char trailingChar) { - Assert.Throws<DirectoryNotFoundException>(() => Delete(GetTestFilePath())); + string path = Path.Combine(GetTestFilePath(), "file" + trailingChar); + Assert.Throws<DirectoryNotFoundException>(() => Delete(path)); } [Fact] diff --git a/src/System.IO.FileSystem/tests/Directory/GetFileSystemEntries_str.cs b/src/System.IO.FileSystem/tests/Directory/GetFileSystemEntries_str.cs index 1e5da1a05c..e6c9b74ef1 100644 --- a/src/System.IO.FileSystem/tests/Directory/GetFileSystemEntries_str.cs +++ b/src/System.IO.FileSystem/tests/Directory/GetFileSystemEntries_str.cs @@ -137,10 +137,18 @@ namespace System.IO.Tests } } - [Fact] - public void NonexistentPath() + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingFile_ThrowsDirectoryNotFound(char trailingChar) + { + string path = GetTestFilePath() + trailingChar; + Assert.Throws<DirectoryNotFoundException>(() => GetEntries(path)); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingDirectory_ThrowsDirectoryNotFound(char trailingChar) { - Assert.Throws<DirectoryNotFoundException>(() => GetEntries(GetTestFilePath())); + string path = Path.Combine(GetTestFilePath(), "file" + trailingChar); + Assert.Throws<DirectoryNotFoundException>(() => GetEntries(path)); } [Fact] diff --git a/src/System.IO.FileSystem/tests/Directory/GetSetTimes.cs b/src/System.IO.FileSystem/tests/Directory/GetSetTimes.cs index 3e94d573fa..f5978b4aab 100644 --- a/src/System.IO.FileSystem/tests/Directory/GetSetTimes.cs +++ b/src/System.IO.FileSystem/tests/Directory/GetSetTimes.cs @@ -120,7 +120,6 @@ namespace System.IO.Tests } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] // Non-existing directory returns default values public void Windows_DirectoryDoesntExist_ReturnDefaultValues() { string path = GetTestFilePath(); @@ -147,34 +146,5 @@ namespace System.IO.Tests Assert.Equal(DateTime.FromFileTimeUtc(0).Ticks, new DirectoryInfo(path).CreationTimeUtc.Ticks); } } - - [Fact] - [PlatformSpecific(TestPlatforms.AnyUnix)] // Non-existing directory causes FileNotFoundException - public void Unix_DirectoryDoesntExist_Throws() - { - string path = GetTestFilePath(); - - //non-utc - Assert.Throws<FileNotFoundException>(() => Directory.GetLastAccessTime(path)); - Assert.Throws<FileNotFoundException>(() => new DirectoryInfo(path).LastAccessTime); - Assert.Throws<FileNotFoundException>(() => Directory.GetLastWriteTime(path)); - Assert.Throws<FileNotFoundException>(() => new DirectoryInfo(path).LastWriteTime); - if (IOInputs.SupportsGettingCreationTime) - { - Assert.Throws<FileNotFoundException>(() => Directory.GetCreationTime(path)); - Assert.Throws<FileNotFoundException>(() => new DirectoryInfo(path).CreationTime); - } - - //utc - Assert.Throws<FileNotFoundException>(() => Directory.GetLastAccessTimeUtc(path)); - Assert.Throws<FileNotFoundException>(() => new DirectoryInfo(path).LastAccessTimeUtc); - Assert.Throws<FileNotFoundException>(() => Directory.GetLastWriteTimeUtc(path)); - Assert.Throws<FileNotFoundException>(() => new DirectoryInfo(path).LastWriteTimeUtc); - if (IOInputs.SupportsGettingCreationTime) - { - Assert.Throws<FileNotFoundException>(() => Directory.GetCreationTimeUtc(path)); - Assert.Throws<FileNotFoundException>(() => new DirectoryInfo(path).CreationTimeUtc); - } - } } } diff --git a/src/System.IO.FileSystem/tests/DirectoryInfo/Exists.cs b/src/System.IO.FileSystem/tests/DirectoryInfo/Exists.cs index e94775c156..d54f799cf0 100644 --- a/src/System.IO.FileSystem/tests/DirectoryInfo/Exists.cs +++ b/src/System.IO.FileSystem/tests/DirectoryInfo/Exists.cs @@ -55,6 +55,14 @@ namespace System.IO.Tests Assert.False(new DirectoryInfo("Da drar vi til fjells").Exists); } + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingDirectory(char trailingChar) + { + string path = GetTestFilePath(); + FileInfo info = new FileInfo(Path.Combine(path, "file" + trailingChar)); + Assert.False(info.Exists); + } + [Fact] [PlatformSpecific(CaseInsensitivePlatforms)] public void CaseInsensitivity() diff --git a/src/System.IO.FileSystem/tests/DirectoryInfo/GetSetAttributes.cs b/src/System.IO.FileSystem/tests/DirectoryInfo/GetSetAttributes.cs index 9910c3f1b3..02b816d679 100644 --- a/src/System.IO.FileSystem/tests/DirectoryInfo/GetSetAttributes.cs +++ b/src/System.IO.FileSystem/tests/DirectoryInfo/GetSetAttributes.cs @@ -38,12 +38,31 @@ namespace System.IO.Tests Assert.Throws<ArgumentException>(() => Set(string.Empty, FileAttributes.Normal)); } - [Fact] - public void NonExistentFile() + // In NetFX we ignore "not found" errors, which leaves the attributes + // state as invalid (0xFFFFFFFF), which makes all flags true. + + [Theory, MemberData(nameof(TrailingCharacters))] + public void GetAttributes_MissingFile(char trailingChar) + { + Assert.Equal((FileAttributes)(-1), Get(GetTestFilePath() + trailingChar)); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void GetAttributes_MissingDirectory(char trailingChar) + { + Assert.Equal((FileAttributes)(-1), Get(Path.Combine(GetTestFilePath(), "file" + trailingChar))); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void SetAttributes_MissingFile(char trailingChar) + { + Assert.Throws<FileNotFoundException>(() => Set(GetTestFilePath() + trailingChar, FileAttributes.ReadOnly)); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void SetAttributes_MissingDirectory(char trailingChar) { - Assert.Throws<FileNotFoundException>(() => Set(GetTestFilePath(), FileAttributes.Normal)); - Assert.Throws<FileNotFoundException>(() => Set(IOServices.AddTrailingSlashIfNeeded(GetTestFilePath()), FileAttributes.Normal)); - Assert.Throws<FileNotFoundException>(() => Set(IOServices.RemoveTrailingSlash(GetTestFilePath()), FileAttributes.Normal)); + Assert.Throws<DirectoryNotFoundException>(() => Set(Path.Combine(GetTestFilePath(), "file" + trailingChar), FileAttributes.ReadOnly)); } [Theory] diff --git a/src/System.IO.FileSystem/tests/File/GetSetAttributes.cs b/src/System.IO.FileSystem/tests/File/GetSetAttributes.cs index ca5f06a2d0..3e3ab2563d 100644 --- a/src/System.IO.FileSystem/tests/File/GetSetAttributes.cs +++ b/src/System.IO.FileSystem/tests/File/GetSetAttributes.cs @@ -137,5 +137,29 @@ namespace System.IO.Tests Set(path, Get(path) & ~FileAttributes.ReadOnly); } } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void GetAttributes_MissingFile(char trailingChar) + { + Assert.Throws<FileNotFoundException>(() => Get(GetTestFilePath() + trailingChar)); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void GetAttributes_MissingDirectory(char trailingChar) + { + Assert.Throws<DirectoryNotFoundException>(() => Get(Path.Combine(GetTestFilePath(), "dir" + trailingChar))); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void SetAttributes_MissingFile(char trailingChar) + { + Assert.Throws<FileNotFoundException>(() => Set(GetTestFilePath() + trailingChar, FileAttributes.ReadOnly)); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void SetAttributes_MissingDirectory(char trailingChar) + { + Assert.Throws<DirectoryNotFoundException>(() => Set(Path.Combine(GetTestFilePath(), "dir" + trailingChar), FileAttributes.ReadOnly)); + } } } diff --git a/src/System.IO.FileSystem/tests/File/GetSetTimes.cs b/src/System.IO.FileSystem/tests/File/GetSetTimes.cs index 89a83ccf53..a89906b145 100644 --- a/src/System.IO.FileSystem/tests/File/GetSetTimes.cs +++ b/src/System.IO.FileSystem/tests/File/GetSetTimes.cs @@ -121,7 +121,6 @@ namespace System.IO.Tests } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] // Non-existing file returns default values public void Windows_FileDoesntExist_ReturnDefaultValues() { string path = GetTestFilePath(); @@ -148,34 +147,5 @@ namespace System.IO.Tests Assert.Equal(DateTime.FromFileTimeUtc(0).Ticks, new FileInfo(path).CreationTimeUtc.Ticks); } } - - [Fact] - [PlatformSpecific(TestPlatforms.AnyUnix)] // Non-existing file throws FileNotFoundException - public void Unix_FileDoesntExist_Throws_FileNotFoundException() - { - string path = GetTestFilePath(); - - //non-utc - Assert.Throws<FileNotFoundException>(() => File.GetLastAccessTime(path)); - Assert.Throws<FileNotFoundException>(() => new FileInfo(path).LastAccessTime); - Assert.Throws<FileNotFoundException>(() => File.GetLastWriteTime(path)); - Assert.Throws<FileNotFoundException>(() => new FileInfo(path).LastWriteTime); - if (IOInputs.SupportsGettingCreationTime) - { - Assert.Throws<FileNotFoundException>(() => File.GetCreationTime(path)); - Assert.Throws<FileNotFoundException>(() => new FileInfo(path).CreationTime); - } - - //utc - Assert.Throws<FileNotFoundException>(() => File.GetLastAccessTimeUtc(path)); - Assert.Throws<FileNotFoundException>(() => new FileInfo(path).LastAccessTimeUtc); - Assert.Throws<FileNotFoundException>(() => File.GetLastWriteTimeUtc(path)); - Assert.Throws<FileNotFoundException>(() => new FileInfo(path).LastWriteTimeUtc); - if (IOInputs.SupportsGettingCreationTime) - { - Assert.Throws<FileNotFoundException>(() => File.GetCreationTimeUtc(path)); - Assert.Throws<FileNotFoundException>(() => new FileInfo(path).CreationTimeUtc); - } - } } } diff --git a/src/System.IO.FileSystem/tests/FileInfo/Exists.cs b/src/System.IO.FileSystem/tests/FileInfo/Exists.cs index ab075a6e43..a7deb781b1 100644 --- a/src/System.IO.FileSystem/tests/FileInfo/Exists.cs +++ b/src/System.IO.FileSystem/tests/FileInfo/Exists.cs @@ -43,6 +43,14 @@ namespace System.IO.Tests Assert.False(new FileInfo("Da drar vi til fjells").Exists); } + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingDirectory(char trailingChar) + { + string path = GetTestFilePath(); + FileInfo info = new FileInfo(Path.Combine(path, "file" + trailingChar)); + Assert.False(info.Exists); + } + [Fact] [PlatformSpecific(CaseInsensitivePlatforms)] public void CaseInsensitivity() diff --git a/src/System.IO.FileSystem/tests/FileInfo/GetSetAttributes.cs b/src/System.IO.FileSystem/tests/FileInfo/GetSetAttributes.cs index 81eb880cd1..c32a0da379 100644 --- a/src/System.IO.FileSystem/tests/FileInfo/GetSetAttributes.cs +++ b/src/System.IO.FileSystem/tests/FileInfo/GetSetAttributes.cs @@ -122,5 +122,32 @@ namespace System.IO.Tests Set(path, attr); Assert.Equal(FileAttributes.Normal, Get(path)); } + + // In NetFX we ignore "not found" errors, which leaves the attributes + // state as invalid (0xFFFFFFFF), which makes all flags true. + + [Theory, MemberData(nameof(TrailingCharacters))] + public void GetAttributes_MissingFile(char trailingChar) + { + Assert.Equal((FileAttributes)(-1), Get(GetTestFilePath() + trailingChar)); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void GetAttributes_MissingDirectory(char trailingChar) + { + Assert.Equal((FileAttributes)(-1), Get(Path.Combine(GetTestFilePath(), "file" + trailingChar))); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void SetAttributes_MissingFile(char trailingChar) + { + Assert.Throws<FileNotFoundException>(() => Set(GetTestFilePath() + trailingChar, FileAttributes.ReadOnly)); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void SetAttributes_MissingDirectory(char trailingChar) + { + Assert.Throws<DirectoryNotFoundException>(() => Set(Path.Combine(GetTestFilePath(), "file" + trailingChar), FileAttributes.ReadOnly)); + } } } diff --git a/src/System.IO.FileSystem/tests/FileInfo/IsReadOnly.cs b/src/System.IO.FileSystem/tests/FileInfo/IsReadOnly.cs new file mode 100644 index 0000000000..2833dc6a09 --- /dev/null +++ b/src/System.IO.FileSystem/tests/FileInfo/IsReadOnly.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; + +namespace System.IO.Tests +{ + public class IsReadOnly : FileSystemTest + { + protected virtual FileSystemInfo Create(string path) + { + return new FileInfo(path); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void NotReadOnly(char trailingChar) + { + string path = GetTestFilePath() + trailingChar; + File.Create(path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)).Dispose(); + FileInfo info = new FileInfo(path); + Assert.False(info.IsReadOnly); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void ReadOnly(char trailingChar) + { + string path = GetTestFilePath() + trailingChar; + string trimmedPath = path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + File.Create(trimmedPath).Dispose(); + var attributes = File.GetAttributes(path); + File.SetAttributes(trimmedPath, attributes | FileAttributes.ReadOnly); + FileInfo info = null; + + try + { + info = new FileInfo(path); + Assert.True(info.IsReadOnly); + } + finally + { + File.SetAttributes(trimmedPath, attributes); + } + + Assert.True(info.IsReadOnly); + info.Refresh(); + Assert.Equal(attributes, File.GetAttributes(path)); + Assert.False(info.IsReadOnly); + } + + // In NetFX we ignore "not found" errors, which leaves the attributes + // state as invalid (0xFFFFFFFF), which makes all flags true. + + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingFile(char trailingChar) + { + string path = GetTestFilePath(); + FileInfo info = new FileInfo(path + trailingChar); + Assert.True(info.IsReadOnly); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingDirectory(char trailingChar) + { + string path = GetTestFilePath(); + FileInfo info = new FileInfo(Path.Combine(path, "file" + trailingChar)); + Assert.True(info.IsReadOnly); + } + } +} diff --git a/src/System.IO.FileSystem/tests/FileInfo/Length.cs b/src/System.IO.FileSystem/tests/FileInfo/Length.cs index 3ae14731c5..2b09ad576d 100644 --- a/src/System.IO.FileSystem/tests/FileInfo/Length.cs +++ b/src/System.IO.FileSystem/tests/FileInfo/Length.cs @@ -42,6 +42,29 @@ namespace System.IO.Tests Assert.Throws<FileNotFoundException>(() => info.Length); } + // Windows returns FILE_NOT_FOUND if the path exists up to the last directory separator, + // or PATH_NOT_FOUND otherwise. Normally we convert those to FileNotFound and + // DirectoryNotFound exceptions, but in this particular case we ignored the actual + // result and always gave FileNotFound. + // + // https://github.com/dotnet/corefx/issues/19850 + + [Theory, MemberData(nameof(TrailingCharacters))] + public void Length_MissingFile_ThrowsFileNotFound(char trailingChar) + { + string path = GetTestFilePath(); + FileInfo info = new FileInfo(path + trailingChar); + Assert.Throws<FileNotFoundException>(() => info.Length); + } + + [Theory, MemberData(nameof(TrailingCharacters))] + public void Length_MissingDirectory_ThrowsFileNotFound(char trailingChar) + { + string path = GetTestFilePath(); + FileInfo info = new FileInfo(Path.Combine(path, "file" + trailingChar)); + Assert.Throws<FileNotFoundException>(() => info.Length); + } + [ConditionalFact(nameof(CanCreateSymbolicLinks))] public void SymLinkLength() { diff --git a/src/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs b/src/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs index 6470b7a99f..cbb3af9594 100644 --- a/src/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs +++ b/src/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Diagnostics; using Xunit; namespace System.IO.Tests { - public class FileStream_ctor_str_fm : RemoteExecutorTestBase + public class FileStream_ctor_str_fm : FileSystemTest { protected virtual FileStream CreateFileStream(string path, FileMode mode) { @@ -38,6 +37,21 @@ namespace System.IO.Tests AssertExtensions.Throws<ArgumentOutOfRangeException>("mode", () => CreateFileStream(GetTestFilePath(), ~FileMode.Open)); } + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingFile_ThrowsFileNotFound(char trailingChar) + { + string path = GetTestFilePath() + trailingChar; + Assert.Throws<FileNotFoundException>(() => CreateFileStream(path, FileMode.Open)); + } + + [ActiveIssue(19965, TestPlatforms.AnyUnix)] + [Theory, MemberData(nameof(TrailingCharacters))] + public void MissingDirectory_ThrowsDirectoryNotFound(char trailingChar) + { + string path = Path.Combine(GetTestFilePath(), "file" + trailingChar); + Assert.Throws<DirectoryNotFoundException>(() => CreateFileStream(path, FileMode.Open)); + } + [Fact] public void FileModeCreate() { diff --git a/src/System.IO.FileSystem/tests/FileSystemTest.cs b/src/System.IO.FileSystem/tests/FileSystemTest.cs index 1386fcbf23..07aaef81f6 100644 --- a/src/System.IO.FileSystem/tests/FileSystemTest.cs +++ b/src/System.IO.FileSystem/tests/FileSystemTest.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics; using Xunit; namespace System.IO.Tests { - public abstract partial class FileSystemTest : FileCleanupTestBase + public abstract partial class FileSystemTest : RemoteExecutorTestBase { public static readonly byte[] TestBuffer = { 0xBA, 0x5E, 0xBA, 0x11, 0xF0, 0x07, 0xBA, 0x11 }; @@ -19,9 +20,9 @@ namespace System.IO.Tests public static bool UsingNewNormalization => !PathFeatures.IsUsingLegacyPathNormalization(); - public static TheoryData<string> PathsWithInvalidColons => TestData.PathsWithInvalidColons; - - public static TheoryData<string> PathsWithInvalidCharacters => TestData.PathsWithInvalidCharacters; + public static TheoryData<string> PathsWithInvalidColons = TestData.PathsWithInvalidColons; + public static TheoryData<string> PathsWithInvalidCharacters = TestData.PathsWithInvalidCharacters; + public static TheoryData<char> TrailingCharacters = TestData.TrailingCharacters; /// <summary> /// In some cases (such as when running without elevated privileges), diff --git a/src/System.IO.FileSystem/tests/Performance/System.IO.FileSystem.Performance.Tests.csproj b/src/System.IO.FileSystem/tests/Performance/System.IO.FileSystem.Performance.Tests.csproj index 2d9fd50a0f..85258bbfe6 100644 --- a/src/System.IO.FileSystem/tests/Performance/System.IO.FileSystem.Performance.Tests.csproj +++ b/src/System.IO.FileSystem/tests/Performance/System.IO.FileSystem.Performance.Tests.csproj @@ -29,6 +29,9 @@ <Compile Include="$(CommonTestPath)\System\IO\PathFeatures.cs"> <Link>Common\System\IO\PathFeatures.cs</Link> </Compile> + <Compile Include="$(CommonTestPath)\System\Diagnostics\RemoteExecutorTestBase.cs"> + <Link>Common\System\Diagnostics\RemoteExecutorTestBase.cs</Link> + </Compile> </ItemGroup> <ItemGroup> <ProjectReference Include="$(CommonPath)\..\perf\PerfRunner\PerfRunner.csproj"> diff --git a/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index 6b1bf119ec..02cbadaf0d 100644 --- a/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -14,12 +14,14 @@ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Windows_NT-Debug|AnyCPU'" /> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Windows_NT-Release|AnyCPU'" /> <ItemGroup> + <Compile Include="FileInfo\IsReadOnly.cs" /> <Compile Include="FileInfo\Replace.cs" /> <Compile Include="FileStream\Handle.cs" /> <Compile Include="Directory\GetLogicalDrives.cs" /> <Compile Include="FileStream\EndRead.cs" /> <Compile Include="FileStream\EndWrite.cs" /> <Compile Include="FileStream\LockUnlock.cs" /> + <Compile Include="FileSystemTest.cs" /> <Compile Include="File\EncryptDecrypt.cs" /> <Compile Include="File\Replace.cs" /> <Compile Include="$(CommonTestPath)\System\Runtime\Serialization\Formatters\BinaryFormatterHelpers.cs"> @@ -98,7 +100,6 @@ <Compile Include="TestData.cs" /> <Compile Include="UnseekableFileStream.cs" /> <Compile Include="FSAssert.cs" /> - <Compile Include="FileSystemTest.cs" /> <!-- Ported --> <Compile Include="PortedCommon\CommonUtilities.cs" /> <Compile Include="PortedCommon\DllImports.cs" /> @@ -175,4 +176,4 @@ </ProjectReference> </ItemGroup> <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" /> -</Project> +</Project>
\ No newline at end of file diff --git a/src/System.IO.FileSystem/tests/TestData.cs b/src/System.IO.FileSystem/tests/TestData.cs index 148c163509..3ca4af5160 100644 --- a/src/System.IO.FileSystem/tests/TestData.cs +++ b/src/System.IO.FileSystem/tests/TestData.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.IO; using System.Runtime.InteropServices; using Xunit; @@ -84,4 +85,25 @@ internal static class TestData return data; } } + + /// <summary> + /// Normal path char and any valid directory separators + /// </summary> + public static TheoryData<char> TrailingCharacters + { + get + { + TheoryData<char> data = new TheoryData<char> + { + // A valid, non separator + 'a', + Path.DirectorySeparatorChar + }; + + if (Path.DirectorySeparatorChar != Path.AltDirectorySeparatorChar) + data.Add(Path.AltDirectorySeparatorChar); + + return data; + } + } } |