using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Test.Common; using FluentAssertions; namespace NzbDrone.Common.Test.DiskTests { [TestFixture] public class DiskTransferServiceFixture : TestBase { private readonly String _sourcePath = @"C:\source\my.video.mkv".AsOsAgnostic(); private readonly String _targetPath = @"C:\target\my.video.mkv".AsOsAgnostic(); private readonly String _backupPath = @"C:\source\my.video.mkv.backup~".AsOsAgnostic(); private readonly String _tempTargetPath = @"C:\target\my.video.mkv.partial~".AsOsAgnostic(); [SetUp] public void SetUp() { Mocker.GetMock(MockBehavior.Strict); WithEmulatedDiskProvider(); WithExistingFile(_sourcePath); } [Test] public void should_hardlink_only() { WithSuccessfulHardlink(_sourcePath, _targetPath); var result = Subject.TransferFile(_sourcePath, _targetPath, TransferMode.HardLink); result.Should().Be(TransferMode.HardLink); } [Test] public void should_throw_if_hardlink_only_failed() { WithFailedHardlink(); Assert.Throws(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.HardLink)); } [Test] public void should_retry_if_partial_copy() { WithSuccessfulHardlink(_sourcePath, _backupPath); var retry = 0; Mocker.GetMock() .Setup(v => v.CopyFile(_sourcePath, _tempTargetPath, false)) .Callback(() => { WithExistingFile(_tempTargetPath, true, 900); if (retry++ == 1) WithExistingFile(_tempTargetPath, true, 1000); }); var result = Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Copy); ExceptionVerification.ExpectedWarns(1); } [Test] public void should_retry_twice_if_partial_copy() { var retry = 0; Mocker.GetMock() .Setup(v => v.CopyFile(_sourcePath, _tempTargetPath, false)) .Callback(() => { WithExistingFile(_tempTargetPath, true, 900); if (retry++ == 3) throw new Exception("Test Failed, retried too many times."); }); Assert.Throws(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Copy)); ExceptionVerification.ExpectedWarns(2); ExceptionVerification.ExpectedErrors(1); } [Test] public void should_hardlink_before_move() { WithSuccessfulHardlink(_sourcePath, _backupPath); var result = Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move); Mocker.GetMock() .Verify(v => v.TryCreateHardLink(_sourcePath, _backupPath), Times.Once()); } [Test] public void should_remove_source_after_move() { WithSuccessfulHardlink(_sourcePath, _backupPath); Mocker.GetMock() .Setup(v => v.MoveFile(_backupPath, _tempTargetPath, false)) .Callback(() => WithExistingFile(_tempTargetPath, true)); var result = Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move); VerifyDeletedFile(_sourcePath); } [Test] public void should_remove_backup_if_move_throws() { WithSuccessfulHardlink(_sourcePath, _backupPath); Mocker.GetMock() .Setup(v => v.MoveFile(_backupPath, _tempTargetPath, false)) .Throws(new IOException("Blackbox IO error")); Assert.Throws(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move)); VerifyDeletedFile(_backupPath); } [Test] public void should_remove_partial_if_move_fails() { WithSuccessfulHardlink(_sourcePath, _backupPath); Mocker.GetMock() .Setup(v => v.MoveFile(_backupPath, _tempTargetPath, false)) .Callback(() => { WithExistingFile(_backupPath, false); WithExistingFile(_tempTargetPath, true, 900); }); Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move); VerifyDeletedFile(_tempTargetPath); } [Test] public void should_fallback_to_copy_if_hardlink_failed() { WithFailedHardlink(); var result = Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move); Mocker.GetMock() .Verify(v => v.CopyFile(_sourcePath, _tempTargetPath, false), Times.Once()); Mocker.GetMock() .Verify(v => v.MoveFile(_tempTargetPath, _targetPath, false), Times.Once()); VerifyDeletedFile(_sourcePath); } [Test] public void CopyFolder_should_copy_folder() { WithRealDiskProvider(); var source = GetFilledTempFolder(); var destination = new DirectoryInfo(GetTempFilePath()); Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy); VerifyCopyFolder(source.FullName, destination.FullName); } [Test] public void CopyFolder_should_overwrite_existing_folder() { WithRealDiskProvider(); var source = GetFilledTempFolder(); var destination = new DirectoryInfo(GetTempFilePath()); Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy); //Delete Random File destination.GetFiles("*.*", SearchOption.AllDirectories).First().Delete(); Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy); VerifyCopyFolder(source.FullName, destination.FullName); } [Test] public void MoveFolder_should_move_folder() { WithRealDiskProvider(); var original = GetFilledTempFolder(); var source = new DirectoryInfo(GetTempFilePath()); var destination = new DirectoryInfo(GetTempFilePath()); Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move); VerifyMoveFolder(original.FullName, source.FullName, destination.FullName); } [Test] public void MoveFolder_should_overwrite_existing_folder() { WithRealDiskProvider(); var original = GetFilledTempFolder(); var source = new DirectoryInfo(GetTempFilePath()); var destination = new DirectoryInfo(GetTempFilePath()); Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); Subject.TransferFolder(original.FullName, destination.FullName, TransferMode.Copy); Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move); VerifyMoveFolder(original.FullName, source.FullName, destination.FullName); } [Test] public void should_throw_if_destination_is_readonly() { Mocker.GetMock() .Setup(v => v.CopyFile(It.IsAny(), It.IsAny(), false)) .Throws(new IOException("Access denied")); Assert.Throws(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Copy)); } [Test] public void should_throw_if_destination_is_child_of_source() { var childPath = Path.Combine(_sourcePath, "child"); Assert.Throws(() => Subject.TransferFile(_sourcePath, childPath, TransferMode.Move)); } public DirectoryInfo GetFilledTempFolder() { var tempFolder = GetTempFilePath(); Directory.CreateDirectory(tempFolder); File.WriteAllText(Path.Combine(tempFolder, Path.GetRandomFileName()), "RootFile"); var subDir = Path.Combine(tempFolder, Path.GetRandomFileName()); Directory.CreateDirectory(subDir); File.WriteAllText(Path.Combine(subDir, Path.GetRandomFileName()), "SubFile1"); File.WriteAllText(Path.Combine(subDir, Path.GetRandomFileName()), "SubFile2"); return new DirectoryInfo(tempFolder); } private void WithExistingFile(string path, bool exists = true, int size = 1000) { Mocker.GetMock() .Setup(v => v.FileExists(path)) .Returns(exists); Mocker.GetMock() .Setup(v => v.GetFileSize(path)) .Returns(size); } private void WithSuccessfulHardlink(string source, string target) { Mocker.GetMock() .Setup(v => v.TryCreateHardLink(source, target)) .Callback(() => WithExistingFile(target)) .Returns(true); } private void WithFailedHardlink() { Mocker.GetMock() .Setup(v => v.TryCreateHardLink(It.IsAny(), It.IsAny())) .Returns(false); } private void WithEmulatedDiskProvider() { Mocker.GetMock() .Setup(v => v.FileExists(It.IsAny())) .Returns(false); Mocker.GetMock() .Setup(v => v.CopyFile(It.IsAny(), It.IsAny(), false)) .Callback((s, d, o) => { WithExistingFile(d); }); Mocker.GetMock() .Setup(v => v.MoveFile(It.IsAny(), It.IsAny(), false)) .Callback((s, d, o) => { WithExistingFile(s, false); WithExistingFile(d); }); Mocker.GetMock() .Setup(v => v.DeleteFile(It.IsAny())) .Callback(v => { WithExistingFile(v, false); }); } private void WithRealDiskProvider() { Mocker.GetMock() .Setup(v => v.FolderExists(It.IsAny())) .Returns(v => Directory.Exists(v)); Mocker.GetMock() .Setup(v => v.FileExists(It.IsAny())) .Returns(v => File.Exists(v)); Mocker.GetMock() .Setup(v => v.CreateFolder(It.IsAny())) .Callback(v => Directory.CreateDirectory(v)); Mocker.GetMock() .Setup(v => v.DeleteFolder(It.IsAny(), It.IsAny())) .Callback((v,r) => Directory.Delete(v, r)); Mocker.GetMock() .Setup(v => v.DeleteFile(It.IsAny())) .Callback(v => File.Delete(v)); Mocker.GetMock() .Setup(v => v.GetDirectoryInfos(It.IsAny())) .Returns(v => new DirectoryInfo(v).GetDirectories().ToList()); Mocker.GetMock() .Setup(v => v.GetFileInfos(It.IsAny())) .Returns(v => new DirectoryInfo(v).GetFiles().ToList()); Mocker.GetMock() .Setup(v => v.GetFileSize(It.IsAny())) .Returns(v => new FileInfo(v).Length); Mocker.GetMock() .Setup(v => v.TryCreateHardLink(It.IsAny(), It.IsAny())) .Returns(false); Mocker.GetMock() .Setup(v => v.CopyFile(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((s, d, o) => File.Copy(s, d, o)); Mocker.GetMock() .Setup(v => v.MoveFile(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((s,d,o) => { if (File.Exists(d) && o) File.Delete(d); File.Move(s, d); }); } private void VerifyCopyFolder(string source, string destination) { var sourceFiles = Directory.GetFileSystemEntries(source, "*", SearchOption.AllDirectories).Select(v => v.Substring(source.Length + 1)).ToArray(); var destFiles = Directory.GetFileSystemEntries(destination, "*", SearchOption.AllDirectories).Select(v => v.Substring(destination.Length + 1)).ToArray(); CollectionAssert.AreEquivalent(sourceFiles, destFiles); } private void VerifyMoveFolder(string source, string from, string destination) { Directory.Exists(from).Should().BeFalse(); var sourceFiles = Directory.GetFileSystemEntries(source, "*", SearchOption.AllDirectories).Select(v => v.Substring(source.Length + 1)).ToArray(); var destFiles = Directory.GetFileSystemEntries(destination, "*", SearchOption.AllDirectories).Select(v => v.Substring(destination.Length + 1)).ToArray(); CollectionAssert.AreEquivalent(sourceFiles, destFiles); } private void VerifyDeletedFile(String filePath) { var path = filePath; Mocker.GetMock() .Verify(v => v.DeleteFile(path), Times.Once()); } } }