From aba057fd1871c35430994cc519a8981f1a9eb0e8 Mon Sep 17 00:00:00 2001 From: Erik Renes Date: Tue, 19 Feb 2019 22:53:57 +0100 Subject: [PATCH 1/4] Initial support for file locking --- .../MockFileLockTests.cs | 166 ++++++++++++++++++ .../MockFile.cs | 15 +- .../MockFileData.cs | 18 ++ .../MockFileSystem.cs | 1 + 4 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs diff --git a/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs b/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs new file mode 100644 index 000000000..9c8ba3e9a --- /dev/null +++ b/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs @@ -0,0 +1,166 @@ +namespace System.IO.Abstractions.TestingHelpers.Tests +{ + using Collections.Generic; + + using NUnit.Framework; + + using XFS = MockUnixSupport; + class MockFileLockTests + { + [Test] + public void MockFile_Lock_FileShareNoneThrows() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.None }} + }); + + Assert.Throws(typeof(IOException), () => filesystem.File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.Read)); + } + [Test] + public void MockFile_Lock_FileShareReadDoesNotThrowOnRead() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.Read }} + }); + + Assert.DoesNotThrow(() => filesystem.File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.Read)); + } + [Test] + public void MockFile_Lock_FileShareReadThrowsOnWrite() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.Read }} + }); + + Assert.Throws(typeof(IOException), () => filesystem.File.Open(filepath, FileMode.Open, FileAccess.Write, FileShare.Read)); + } + [Test] + public void MockFile_Lock_FileShareWriteThrowsOnRead() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.Write }} + }); + + Assert.Throws(typeof(IOException), () => filesystem.File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.Read)); + } + [Test] + public void MockFile_Lock_FileShareWriteDoesNotThrowOnWrite() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.Write }} + }); + + Assert.DoesNotThrow(() => filesystem.File.Open(filepath, FileMode.Open, FileAccess.Write, FileShare.Read)); + } + + + [Test] + public void MockFile_Lock_FileShareNoneThrowsOnOpenRead() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.None }} + }); + + var exception = Assert.Throws(typeof(IOException), () => filesystem.File.OpenRead(filepath)); + Assert.That(exception.Message, Is.EqualTo($"The process cannot access the file '{filepath}' because it is being used by another process.")); + } + [Test] + public void MockFile_Lock_FileShareNoneThrowsOnWriteAllLines() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.None }} + }); + + var exception = Assert.Throws(typeof(IOException), () => filesystem.File.WriteAllLines(filepath, new string[] { "hello", "world" })); + Assert.That(exception.Message, Is.EqualTo($"The process cannot access the file '{filepath}' because it is being used by another process.")); + } + [Test] + public void MockFile_Lock_FileShareNoneThrowsOnReadAllLines() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.None }} + }); + + var exception = Assert.Throws(typeof(IOException), () => filesystem.File.ReadAllLines(filepath)); + Assert.That(exception.Message, Is.EqualTo($"The process cannot access the file '{filepath}' because it is being used by another process.")); + } + [Test] + public void MockFile_Lock_FileShareNoneThrowsOnReadAllText() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.None }} + }); + + var exception = Assert.Throws(typeof(IOException), () => filesystem.File.ReadAllText(filepath)); + Assert.That(exception.Message, Is.EqualTo($"The process cannot access the file '{filepath}' because it is being used by another process.")); + } + [Test] + public void MockFile_Lock_FileShareNoneThrowsOnReadAllBytes() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.None }} + }); + + var exception = Assert.Throws(typeof(IOException), () => filesystem.File.ReadAllBytes(filepath)); + Assert.That(exception.Message, Is.EqualTo($"The process cannot access the file '{filepath}' because it is being used by another process.")); + } + [Test] + public void MockFile_Lock_FileShareNoneThrowsOnAppendLines() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.None }} + }); + + var exception = Assert.Throws(typeof(IOException), () => filesystem.File.AppendAllLines(filepath, new string[] { "hello", "world" })); + Assert.That(exception.Message, Is.EqualTo($"The process cannot access the file '{filepath}' because it is being used by another process.")); + } + + [Test] + public void MockFile_Lock_FileShareNoneThrowsFileMove() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + string target = XFS.Path(@"c:\something\does\notexist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.None }} + }); + + var exception = Assert.Throws(typeof(IOException), () => filesystem.File.Move(filepath, target)); + Assert.That(exception.Message, Is.EqualTo("The process cannot access the file because it is being used by another process.")); + } + [Test] + public void MockFile_Lock_FileShareDeleteDoesNotThrowFileMove() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + string target = XFS.Path(@"c:\something\does\notexist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.Delete }} + }); + + Assert.DoesNotThrow(() => filesystem.File.Move(filepath, target)); + } + } +} diff --git a/System.IO.Abstractions.TestingHelpers/MockFile.cs b/System.IO.Abstractions.TestingHelpers/MockFile.cs index 1ca3ac9bf..7c0b59960 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFile.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFile.cs @@ -61,6 +61,7 @@ public override void AppendAllText(string path, string contents, Encoding encodi else { var file = mockFileDataAccessor.GetFile(path); + file.CheckFileAccess(path, FileAccess.Write); var bytesToAppend = encoding.GetBytes(contents); file.Contents = file.Contents.Concat(bytesToAppend).ToArray(); } @@ -354,7 +355,10 @@ public override void Move(string sourceFileName, string destFileName) { throw CommonExceptions.FileNotFound(sourceFileName); } - + if (!sourceFile.AllowedFileShare.HasFlag(FileShare.Delete)) + { + throw new IOException("The process cannot access the file because it is being used by another process."); + } VerifyDirectoryExists(destFileName); mockFileDataAccessor.AddFile(destFileName, new MockFileData(sourceFile.Contents)); @@ -404,8 +408,10 @@ private Stream OpenInternal( return Create(path); } - var length = mockFileDataAccessor.GetFile(path).Contents.Length; + var mockFileData = mockFileDataAccessor.GetFile(path); + mockFileData.CheckFileAccess(path, access); + var length = mockFileData.Contents.Length; MockFileStream.StreamType streamType = MockFileStream.StreamType.WRITE; if (access == FileAccess.Read) streamType = MockFileStream.StreamType.READ; @@ -446,7 +452,7 @@ public override byte[] ReadAllBytes(string path) { throw CommonExceptions.FileNotFound(path); } - + mockFileDataAccessor.GetFile(path).CheckFileAccess(path, FileAccess.Read); return mockFileDataAccessor.GetFile(path).Contents; } @@ -458,6 +464,7 @@ public override string[] ReadAllLines(string path) { throw CommonExceptions.FileNotFound(path); } + mockFileDataAccessor.GetFile(path).CheckFileAccess(path, FileAccess.Read); return mockFileDataAccessor .GetFile(path) @@ -479,6 +486,7 @@ public override string[] ReadAllLines(string path, Encoding encoding) throw CommonExceptions.FileNotFound(path); } + mockFileDataAccessor.GetFile(path).CheckFileAccess(path, FileAccess.Read); return encoding .GetString(mockFileDataAccessor.GetFile(path).Contents) .SplitLines(); @@ -960,6 +968,7 @@ internal static string ReadAllBytes(byte[] contents, Encoding encoding) private string ReadAllTextInternal(string path, Encoding encoding) { var mockFileData = mockFileDataAccessor.GetFile(path); + mockFileData.CheckFileAccess(path, FileAccess.Read); return ReadAllBytes(mockFileData.Contents, encoding); } diff --git a/System.IO.Abstractions.TestingHelpers/MockFileData.cs b/System.IO.Abstractions.TestingHelpers/MockFileData.cs index 15aa84298..55e4c687d 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFileData.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFileData.cs @@ -161,5 +161,23 @@ public FileSecurity AccessControl } set { accessControl = value; } } + + /// + /// Gets or sets the File sharing mode for this file, this allows you to lock a file for reading or writing. + /// + public FileShare AllowedFileShare { get; set; } = FileShare.ReadWrite | FileShare.Delete; + /// + /// Checks whether the file is accessible for this type of FileAccess. + /// MockfileData can be configured to have FileShare.None, which indicates it is locked by a 'different process'. + /// + /// If the file is 'locked by a different process', an IOException will be thrown. + /// + /// The path is used in the IOException message to match the message in real life situations + /// The access type to check + internal void CheckFileAccess(string path, FileAccess access) + { + if (!AllowedFileShare.HasFlag((FileShare)access)) + throw new IOException($"The process cannot access the file '{path}' because it is being used by another process."); + } } } diff --git a/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs b/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs index d4bde1869..8e4edc7d1 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs @@ -129,6 +129,7 @@ public void AddFile(string path, MockFileData mockFile) { throw CommonExceptions.AccessDenied(path); } + file.CheckFileAccess(fixedPath, FileAccess.Write); } var directoryPath = Path.GetDirectoryName(fixedPath); From fe2de986a069c7af243c9ef9c855e0a4cf1dee9f Mon Sep 17 00:00:00 2001 From: Erik Renes Date: Wed, 20 Feb 2019 11:37:01 +0100 Subject: [PATCH 2/4] Verified FileShare behavior of File.Delete and implemented the same. --- .../MockFileLockTests.cs | 23 +++++++++++++++++++ .../MockFile.cs | 6 +++++ 2 files changed, 29 insertions(+) diff --git a/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs b/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs index 9c8ba3e9a..4ba64b9b3 100644 --- a/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs +++ b/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs @@ -162,5 +162,28 @@ public void MockFile_Lock_FileShareDeleteDoesNotThrowFileMove() Assert.DoesNotThrow(() => filesystem.File.Move(filepath, target)); } + [Test] + public void MockFile_Lock_FileShareNoneThrowsDelete() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.None }} + }); + + var exception = Assert.Throws(typeof(IOException), () => filesystem.File.Delete(filepath)); + Assert.That(exception.Message, Is.EqualTo($"The process cannot access the file '{filepath}' because it is being used by another process.")); + } + [Test] + public void MockFile_Lock_FileShareDeleteDoesNotThrowDelete() + { + string filepath = XFS.Path(@"c:\something\does\exist.txt"); + var filesystem = new MockFileSystem(new Dictionary + { + { filepath, new MockFileData("I'm here") { AllowedFileShare = FileShare.Delete }} + }); + + Assert.DoesNotThrow(() => filesystem.File.Delete(filepath)); + } } } diff --git a/System.IO.Abstractions.TestingHelpers/MockFile.cs b/System.IO.Abstractions.TestingHelpers/MockFile.cs index 7c0b59960..74a015bf8 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFile.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFile.cs @@ -177,6 +177,12 @@ public override void Delete(string path) // but silently returns if deleting a non-existing file in an existing folder. VerifyDirectoryExists(path); + var file = mockFileDataAccessor.GetFile(path); + if (file != null && !file.AllowedFileShare.HasFlag(FileShare.Delete)) + { + throw new IOException($"The process cannot access the file '{path}' because it is being used by another process."); + } + mockFileDataAccessor.RemoveFile(path); } From 70f1f757c37b3c0eb4b5463be382fb88a0e4f7d2 Mon Sep 17 00:00:00 2001 From: erenes Date: Sat, 9 Mar 2019 20:58:18 +0100 Subject: [PATCH 3/4] Moved IOException to the CommonExceptions class --- System.IO.Abstractions.TestingHelpers/CommonExceptions.cs | 3 +++ System.IO.Abstractions.TestingHelpers/MockFile.cs | 2 +- .../Properties/Resources.resx | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs b/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs index 66bdc287b..a65be57fc 100644 --- a/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs +++ b/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs @@ -54,5 +54,8 @@ public static ArgumentException IllegalCharactersInPath(string paramName = null) public static Exception InvalidUncPath(string paramName) => new ArgumentException(@"The UNC path should be of the form \\server\share.", paramName); + + public static IOException ProcessCannotAccessFileInUse() => + new IOException(StringResources.Manager.GetString("PROCESS_CANNOT_ACCESS_FILE_IN_USE")); } } \ No newline at end of file diff --git a/System.IO.Abstractions.TestingHelpers/MockFile.cs b/System.IO.Abstractions.TestingHelpers/MockFile.cs index 74a015bf8..52ce256c4 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFile.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFile.cs @@ -363,7 +363,7 @@ public override void Move(string sourceFileName, string destFileName) } if (!sourceFile.AllowedFileShare.HasFlag(FileShare.Delete)) { - throw new IOException("The process cannot access the file because it is being used by another process."); + throw CommonExceptions.ProcessCannotAccessFileInUse(); } VerifyDirectoryExists(destFileName); diff --git a/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx b/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx index 13097ff13..b22ade6cf 100644 --- a/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx +++ b/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx @@ -147,4 +147,7 @@ MockFileSystem does not have a built-in FileSystemWatcher implementation. You must provide your own mock or implementation of IFileSystemWatcherFactory and assign it to MockFileSystem.FileSystemWatcher. + + The process cannot access the file because it is being used by another process. + From 58b7a2ea7f2a8911d086662b1b710f59037cfd24 Mon Sep 17 00:00:00 2001 From: erenes Date: Sat, 9 Mar 2019 21:10:41 +0100 Subject: [PATCH 4/4] Also move the File-sharing exception with filename to the CommonExceptions class --- System.IO.Abstractions.TestingHelpers/CommonExceptions.cs | 6 ++++-- System.IO.Abstractions.TestingHelpers/MockFile.cs | 2 +- System.IO.Abstractions.TestingHelpers/MockFileData.cs | 2 +- .../Properties/Resources.resx | 3 +++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs b/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs index a65be57fc..c42acc894 100644 --- a/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs +++ b/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs @@ -55,7 +55,9 @@ public static ArgumentException IllegalCharactersInPath(string paramName = null) public static Exception InvalidUncPath(string paramName) => new ArgumentException(@"The UNC path should be of the form \\server\share.", paramName); - public static IOException ProcessCannotAccessFileInUse() => - new IOException(StringResources.Manager.GetString("PROCESS_CANNOT_ACCESS_FILE_IN_USE")); + public static IOException ProcessCannotAccessFileInUse(string paramName = null) => + paramName != null + ? new IOException(string.Format(StringResources.Manager.GetString("PROCESS_CANNOT_ACCESS_FILE_IN_USE_WITH_FILENAME"), paramName)) + : new IOException(StringResources.Manager.GetString("PROCESS_CANNOT_ACCESS_FILE_IN_USE")); } } \ No newline at end of file diff --git a/System.IO.Abstractions.TestingHelpers/MockFile.cs b/System.IO.Abstractions.TestingHelpers/MockFile.cs index 52ce256c4..27ecbe2da 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFile.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFile.cs @@ -180,7 +180,7 @@ public override void Delete(string path) var file = mockFileDataAccessor.GetFile(path); if (file != null && !file.AllowedFileShare.HasFlag(FileShare.Delete)) { - throw new IOException($"The process cannot access the file '{path}' because it is being used by another process."); + throw CommonExceptions.ProcessCannotAccessFileInUse(path); } mockFileDataAccessor.RemoveFile(path); diff --git a/System.IO.Abstractions.TestingHelpers/MockFileData.cs b/System.IO.Abstractions.TestingHelpers/MockFileData.cs index 55e4c687d..7aae029ab 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFileData.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFileData.cs @@ -177,7 +177,7 @@ public FileSecurity AccessControl internal void CheckFileAccess(string path, FileAccess access) { if (!AllowedFileShare.HasFlag((FileShare)access)) - throw new IOException($"The process cannot access the file '{path}' because it is being used by another process."); + throw CommonExceptions.ProcessCannotAccessFileInUse(path); } } } diff --git a/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx b/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx index b22ade6cf..76b81e948 100644 --- a/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx +++ b/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx @@ -150,4 +150,7 @@ The process cannot access the file because it is being used by another process. + + The process cannot access the file '{0}' because it is being used by another process. +