diff --git a/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs b/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs new file mode 100644 index 000000000..4ba64b9b3 --- /dev/null +++ b/System.IO.Abstractions.TestingHelpers.Tests/MockFileLockTests.cs @@ -0,0 +1,189 @@ +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)); + } + [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/CommonExceptions.cs b/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs index 66bdc287b..c42acc894 100644 --- a/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs +++ b/System.IO.Abstractions.TestingHelpers/CommonExceptions.cs @@ -54,5 +54,10 @@ 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(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 1ca3ac9bf..27ecbe2da 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(); } @@ -176,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 CommonExceptions.ProcessCannotAccessFileInUse(path); + } + mockFileDataAccessor.RemoveFile(path); } @@ -354,7 +361,10 @@ public override void Move(string sourceFileName, string destFileName) { throw CommonExceptions.FileNotFound(sourceFileName); } - + if (!sourceFile.AllowedFileShare.HasFlag(FileShare.Delete)) + { + throw CommonExceptions.ProcessCannotAccessFileInUse(); + } VerifyDirectoryExists(destFileName); mockFileDataAccessor.AddFile(destFileName, new MockFileData(sourceFile.Contents)); @@ -404,8 +414,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 +458,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 +470,7 @@ public override string[] ReadAllLines(string path) { throw CommonExceptions.FileNotFound(path); } + mockFileDataAccessor.GetFile(path).CheckFileAccess(path, FileAccess.Read); return mockFileDataAccessor .GetFile(path) @@ -479,6 +492,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 +974,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..7aae029ab 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 CommonExceptions.ProcessCannotAccessFileInUse(path); + } } } 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); diff --git a/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx b/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx index 13097ff13..76b81e948 100644 --- a/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx +++ b/System.IO.Abstractions.TestingHelpers/Properties/Resources.resx @@ -147,4 +147,10 @@ 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. + + + The process cannot access the file '{0}' because it is being used by another process. +