From f3f9eb024efa6cbd77628f06ecb7c398fd5513cc Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:04:50 -0400 Subject: [PATCH 01/16] Implement Mocks for consumer unit testing --- .../Mocks/MockFileCopierFactory.cs | 55 +++++++++ RoboSharp.Extensions/Mocks/MockIFileCopier.cs | 116 ++++++++++++++++++ .../Mocks/MockProcessedDirectoryPair.cs | 28 +++++ .../Mocks/MockProcessedFilePair.cs | 26 ++++ RoboSharp.Extensions/Mocks/MockRoboCommand.cs | 46 +++++++ 5 files changed, 271 insertions(+) create mode 100644 RoboSharp.Extensions/Mocks/MockFileCopierFactory.cs create mode 100644 RoboSharp.Extensions/Mocks/MockIFileCopier.cs create mode 100644 RoboSharp.Extensions/Mocks/MockProcessedDirectoryPair.cs create mode 100644 RoboSharp.Extensions/Mocks/MockProcessedFilePair.cs create mode 100644 RoboSharp.Extensions/Mocks/MockRoboCommand.cs diff --git a/RoboSharp.Extensions/Mocks/MockFileCopierFactory.cs b/RoboSharp.Extensions/Mocks/MockFileCopierFactory.cs new file mode 100644 index 00000000..a9cf7e23 --- /dev/null +++ b/RoboSharp.Extensions/Mocks/MockFileCopierFactory.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#nullable enable +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace RoboSharp.Extensions.Mocks +{ + /// + /// A mock that is designed for unit testing scenarios. + /// - Creates objects that will not perform copy or move operations. + /// + public class MockFileCopierFactory : IFileCopierFactory + { + public static MockFileCopierFactory Instance => instance ??= new(); + private static MockFileCopierFactory? instance; + + + public IFileCopier Create(FileInfo source, FileInfo destination, IDirectoryPair? parent) + { + return new MockIFileCopier() + { + Source = source, + Destination = destination, + Parent = new MockProcessedDirectoryPair() { Source = parent?.Source, Destination = parent?.Destination } + }; + } + + public IFileCopier Create(IFileSource fileSource, string destination) => Create(fileSource.FilePath, destination); + public IFileCopier Create(string source, string destination, IDirectoryPair? parent) => Create(new FileInfo(source), new FileInfo(destination), parent); + public IFileCopier Create(string source, string destination) => Create(new FileInfo(source), new FileInfo(destination), null); + public IFileCopier Create(FileInfo source, FileInfo destination) => Create(source, destination, null); + public IFileCopier Create(IFilePair filePair) => Create(filePair.Source, filePair.Destination); + + + + public IFileCopier Create(string source, DirectoryInfo destination, IDirectoryPair? parent = null) + { + return new MockIFileCopier() + { + Source = new FileInfo(source), + Destination = new FileInfo(Path.Combine( destination.FullName, source)), + Parent = new MockProcessedDirectoryPair() { Source = parent?.Source, Destination = parent?.Destination } + }; + } + public IFileCopier Create(IFileSource fileSource, DirectoryInfo destination) => Create(fileSource.FilePath, destination, null); + public IFileCopier Create(FileInfo source, DirectoryInfo destination, IDirectoryPair? parent = null) => Create(source.Name, destination, parent); + + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/RoboSharp.Extensions/Mocks/MockIFileCopier.cs b/RoboSharp.Extensions/Mocks/MockIFileCopier.cs new file mode 100644 index 00000000..add36b86 --- /dev/null +++ b/RoboSharp.Extensions/Mocks/MockIFileCopier.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace RoboSharp.Extensions.Mocks +{ + /// + /// A mock that is designed for unit testing scenarios. + /// - No actual copy operation takes place. Source/Destination are not required to be set. + /// - Customize with and + /// + public class MockIFileCopier : IFileCopier, IFileSource + { + /// + /// The timespan to use with to simulate a copy or move + /// + /// Default = 3ms + public TimeSpan CopyDelay { get; set; } = new TimeSpan(0, 0, 0, 0, 3); + + /// + /// What to return from the Copy/Move tasks + /// + public bool ReturnValue { get; set; } = true; + + + public bool IsCopying { get; private set; } + + public bool IsPaused { get; private set; } + + public DateTime StartDate { get; private set; } + + public DateTime EndDate { get; private set; } + + public IProcessedDirectoryPair? Parent { get; set; } + + public ProcessedFileInfo? ProcessedFileInfo { get; set; } + + public bool ShouldCopy { get; set; } = true; + + public bool ShouldPurge { get; set; } + + /// + /// Not Required to be set + /// + /// + public FileInfo? Source { get; set; } + + /// + /// Not Required to be set + /// + /// + public FileInfo? Destination { get; set; } + + string IFileSource.FilePath => Source?.FullName ?? throw new InvalidOperationException($"{nameof(MockIFileCopier)}.{nameof(Source)} property is not set."); + + public event EventHandler? ProgressUpdated; + + private CancellationTokenSource? mockCts; + + public void Cancel() + { + mockCts?.Cancel(); + } + + public Task CopyAsync() => CopyAsync(false, default); + + public Task CopyAsync(CancellationToken token) => CopyAsync(false, default); + + public Task CopyAsync(bool overwrite) => CopyAsync(overwrite, default); + + public async Task CopyAsync(bool overwrite, CancellationToken token) + { + if (ShouldCopy) + { + IsCopying = true; + IsPaused = false; + StartDate = DateTime.Now; + mockCts = new CancellationTokenSource(); + var cts = CancellationTokenSource.CreateLinkedTokenSource(token, mockCts.Token); + await Task.Delay(CopyDelay, cts.Token); + IsCopying = false; + cts.Dispose(); + ProgressUpdated?.Invoke(this, new CopyProgressEventArgs(100)); + EndDate = DateTime.Now; + return ReturnValue; + } + return false; + } + + public Task MoveAsync() => MoveAsync(false, default); + + public Task MoveAsync(CancellationToken token) => CopyAsync(false, token); + + public Task MoveAsync(bool overwrite) => MoveAsync(overwrite, default); + + public Task MoveAsync(bool overwrite, CancellationToken token) => CopyAsync(overwrite, token); + + public void Pause() + { + + } + + public void Resume() + { + + } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/RoboSharp.Extensions/Mocks/MockProcessedDirectoryPair.cs b/RoboSharp.Extensions/Mocks/MockProcessedDirectoryPair.cs new file mode 100644 index 00000000..6e043011 --- /dev/null +++ b/RoboSharp.Extensions/Mocks/MockProcessedDirectoryPair.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#nullable enable +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace RoboSharp.Extensions.Mocks +{ + /// + /// A mock where all properties are settable + /// + public class MockProcessedDirectoryPair : IProcessedDirectoryPair + { + private ProcessedFileInfo? info; + public ProcessedFileInfo? ProcessedFileInfo + { + get => info ??= new ProcessedFileInfo() { Name = Source?.FullName ?? "", FileClassType = FileClassType.NewDir, Size = 0 }; + set => info = value; + } + public DirectoryInfo? Source { get; set; } + public DirectoryInfo? Destination { get; set; } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member diff --git a/RoboSharp.Extensions/Mocks/MockProcessedFilePair.cs b/RoboSharp.Extensions/Mocks/MockProcessedFilePair.cs new file mode 100644 index 00000000..4cb8e063 --- /dev/null +++ b/RoboSharp.Extensions/Mocks/MockProcessedFilePair.cs @@ -0,0 +1,26 @@ +using System.IO; + +#nullable enable +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace RoboSharp.Extensions.Mocks +{ + /// + /// A mock where all properties are settable + /// + public class MockProcessedFilePair : IProcessedFilePair + { + private ProcessedFileInfo? info; + public ProcessedFileInfo? ProcessedFileInfo + { + get => info ??= new ProcessedFileInfo() { Name = Source?.FullName ?? "", FileClassType = FileClassType.File, Size = Source?.Length ?? 0 }; + set => info = value; + } + public IProcessedDirectoryPair? Parent { get; set; } + public bool ShouldCopy { get; set; } = true; + public bool ShouldPurge { get; set; } + public FileInfo? Source { get; set; } + public FileInfo? Destination { get; set; } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member diff --git a/RoboSharp.Extensions/Mocks/MockRoboCommand.cs b/RoboSharp.Extensions/Mocks/MockRoboCommand.cs new file mode 100644 index 00000000..b46fb502 --- /dev/null +++ b/RoboSharp.Extensions/Mocks/MockRoboCommand.cs @@ -0,0 +1,46 @@ +using RoboSharp.Extensions.Helpers; +using RoboSharp.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace RoboSharp.Extensions.Mocks +{ + /// + /// A mock + ///
Starting only returns + ///
+ public class MockRoboCommand : AbstractIRoboCommand + { + public override void Dispose() + { + + } + + public override Task Start(string domain = "", string username = "", string password = "") + { + var resultsbuilder = new ResultsBuilder(this); + base.ListOnlyResults = resultsbuilder.GetResults(); + base.RunResults = ListOnlyResults; + return Task.CompletedTask; + } + + public override void Stop() + { + + } + + public class Factory : RoboCommandFactory + { + public override IRoboCommand GetRoboCommand() + { + return new MockRoboCommand(); + } + } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file From fc7f6e06903cb37e5ec61f9e7dba95fc7ace8fcd Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:07:37 -0400 Subject: [PATCH 02/16] Create FactoryCommand and FactoryCommandFactory - This new IRoboCommand utilizes the IFileCopierFactory to perform copy operations without creating a new RoboCopy process. This allows full control over the actual copy operation if desired, and allows for cross-platform targeting if needed. - Added IAuthenticator to allow customization of the FactoryCommand's authentication prior to starting the command. By default simply verifies it can access source and destination. - Adjusted helpers and extension methods as needed. --- .../FactoryCommandTests.cs | 354 +++++++++++ RoboSharp.Extensions/FactoryCommand.cs | 579 ++++++++++++++++++ RoboSharp.Extensions/FactoryCommandFactory.cs | 70 +++ .../Helpers/IDirectoryPairExtensions.cs | 48 +- .../Helpers/IFilePairExtensions.cs | 120 +++- .../Helpers/ResultsBuilder.cs | 3 +- RoboSharp.Extensions/IAuthenticator.cs | 62 ++ RoboSharp/Authentication.cs | 6 +- .../RoboSharpConfig_EN.cs | 2 +- RoboSharp/ProcessedFileInfo.cs | 7 +- RoboSharp/RoboSharpConfiguration.cs | 2 +- 11 files changed, 1242 insertions(+), 11 deletions(-) create mode 100644 RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs create mode 100644 RoboSharp.Extensions/FactoryCommand.cs create mode 100644 RoboSharp.Extensions/FactoryCommandFactory.cs create mode 100644 RoboSharp.Extensions/IAuthenticator.cs diff --git a/RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs b/RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs new file mode 100644 index 00000000..ebeb77a4 --- /dev/null +++ b/RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs @@ -0,0 +1,354 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RoboSharp.Extensions.Helpers; +using RoboSharp.Extensions.Tests; +using RoboSharp.Interfaces; +using RoboSharp.UnitTests; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#if NET8_0_OR_GREATER + +namespace RoboSharp.Extensions.Tests +{ + /// + /// Test the object + /// + [TestClass] + public class FactoryCommand_EventTests : RoboSharp.UnitTests.RoboCommandEventTests + { + protected override IRoboCommand GenerateCommand(bool UseLargerFileSet, bool ListOnlyMode) + { + var rc = RoboSharp.UnitTests.Test_Setup.GenerateCommand(false, true); + var command = new FactoryCommand(RoboSharp.Extensions.StreamedCopierFactory.DefaultFactory) + { + CopyOptions = rc.CopyOptions, + SelectionOptions = rc.SelectionOptions, + RetryOptions = rc.RetryOptions, + LoggingOptions = rc.LoggingOptions, + Configuration = rc.Configuration, + }; + return command; + } + } + + /// + /// Validate that the command works the same as robocopy + /// + [TestClass] + public class FactoryCommand_Tests + { + const LoggingFlags DefaultLoggingAction = LoggingFlags.RoboSharpDefault | LoggingFlags.NoJobHeader; + + private static FactoryCommand GetCommand(RoboCommand rc, IFileCopierFactory factory = null) + { + return new FactoryCommand(factory ?? RoboSharp.Extensions.StreamedCopierFactory.DefaultFactory) + { + CopyOptions = rc.CopyOptions, + SelectionOptions = rc.SelectionOptions, + RetryOptions = rc.RetryOptions, + LoggingOptions = rc.LoggingOptions, + Configuration = rc.Configuration, + }; + } + + static string GetMoveSource() + { + string original = TestPrep.SourceDirPath; + return Path.Combine(original.Replace(Path.GetFileName(original), ""), "MoveSource"); + } + + [DataRow(true, @"C:\SomeDir")] + [DataRow(false, @"D:\System Volume Information")] + [TestMethod] + [Timeout(10000)] + public void IsAllowedDir(bool expected, string path) + { + Assert.AreEqual(expected, RoboMover.IsAllowedRootDirectory(new DirectoryInfo(path))); + } + + /// + /// Copy Test will use a standard ROBOCOPY command + /// + [TestMethod] + [Timeout(10000)] + [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Mirror }, DisplayName = "Mirror")] + [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Default }, DisplayName = "Defaults")] + [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectories }, DisplayName = "Subdirectories")] + [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectoriesIncludingEmpty }, DisplayName = "EmptySubdirectories")] + public void CopyTest(object[] flags) + { + Test_Setup.ClearOutTestDestination(); + CopyActionFlags copyAction = (CopyActionFlags)flags[2]; + SelectionFlags selectionFlags = (SelectionFlags)flags[1]; + LoggingFlags loggingAction = (LoggingFlags)flags[0]; + + var rc = TestPrep.GetRoboCommand(false, copyAction, selectionFlags, loggingAction); + var crc = GetCommand(rc); + + rc.LoggingOptions.ListOnly = true; + var results1 = TestPrep.RunTests(rc, crc, false).Result; + TestPrep.CompareTestResults(results1[0], results1[1], rc.LoggingOptions.ListOnly); + + rc.LoggingOptions.ListOnly = false; + var results2 = TestPrep.RunTests(rc, crc, true).Result; + TestPrep.CompareTestResults(results2[0], results2[1], rc.LoggingOptions.ListOnly); + } + + private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags selectionFlags, LoggingFlags loggingFlags, out RoboCommand rc, out FactoryCommand rm) + { + rc = TestPrep.GetRoboCommand(false, copyFlags, selectionFlags, loggingFlags); + rc.CopyOptions.Source = GetMoveSource(); + rm = GetCommand(rc, Mocks.MockFileCopierFactory.Instance); + } + + private static void PrepMoveFiles() + { + var rc = TestPrep.GetRoboCommand(false, CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction); + rc.CopyOptions.Destination = GetMoveSource(); + Directory.CreateDirectory(rc.CopyOptions.Destination); + rc.Start().Wait(); + var results = rc.GetResults(); + if (results.RoboCopyErrors.Length > 0) + throw new Exception( + "Prep Failed \n" + + string.Concat(args: results.RoboCopyErrors.Select(e => "\n RoboCommandError :\t" + e.GetType() + "\t" + e.ErrorDescription + "\t:\t" + e.ErrorPath).ToArray()) + + "\n" + ); + } + + private const CopyActionFlags Mov_ = CopyActionFlags.MoveFiles; + private const CopyActionFlags Move = CopyActionFlags.MoveFilesAndDirectories; + + /// + /// This uses the actual logic provided by the RoboMover object + /// + [TestMethod] + [Timeout(10000)] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] + [DataRow(data: new object[] { Mov_ | CopyActionFlags.CopySubdirectories, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories | Move Files")] + [DataRow(data: new object[] { Move | CopyActionFlags.CopySubdirectories, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories | Move Files and Directories")] + [DataRow(data: new object[] { Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories-Empty | Move Files")] + [DataRow(data: new object[] { Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories-Empty | Move Files and Directories")] + public void MoveTest(object[] flags) + { + if (Test_Setup.IsRunningOnAppVeyor()) return; + GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); + bool listOnly = rc.LoggingOptions.ListOnly; + var results1 = TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles).Result; + TestPrep.CompareTestResults(results1[0], results1[1], listOnly); + } + + [TestMethod] + [Timeout(10000)] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] + public void FileInclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + { + if (Test_Setup.IsRunningOnAppVeyor()) return; + GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); + bool listOnly = rc.LoggingOptions.ListOnly; + rc.CopyOptions.FileFilter = new string[] { "*.txt" }; + var results1 = TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles).Result; + TestPrep.CompareTestResults(results1[0], results1[1], listOnly); + } + + [TestMethod] + [Timeout(10000)] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] + public void FileExclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + { + if (Test_Setup.IsRunningOnAppVeyor()) return; + GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); + rc.SelectionOptions.ExcludedFiles.Add("*.txt"); + rc.Configuration.EnableFileLogging = true; + bool listOnly = rc.LoggingOptions.ListOnly; + var results1 = TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles).Result; + TestPrep.CompareTestResults(results1[0], results1[1], listOnly); + } + + + [TestMethod] + [Timeout(10000)] + [DataRow(data: new object[] { Move | CopyActionFlags.CopySubdirectories, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Include Subdirectories")] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] + public void ExtraFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + { + if (Test_Setup.IsRunningOnAppVeyor()) return; + GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2] | LoggingFlags.ReportExtraFiles, out var rc, out var rm); + bool listOnly = rc.LoggingOptions.ListOnly; + var results1 = TestPrep.RunTests(rc, rm, !listOnly, CreateFile).Result; + TestPrep.CompareTestResults(results1[0], results1[1], listOnly); + + void CreateFile() + { + PrepMoveFiles(); + string path = Path.Combine(TestPrep.DestDirPath, "ExtraFileTest.txt"); + if (!File.Exists(path)) + { + Directory.CreateDirectory(TestPrep.DestDirPath); + File.WriteAllText(path, "This is an extra file"); + } + } + } + + [TestMethod] + [Timeout(10000)] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] + [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] + [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] + public void SameFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + { + if (Test_Setup.IsRunningOnAppVeyor()) return; + GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); + bool listOnly = rc.LoggingOptions.ListOnly; + var results1 = TestPrep.RunTests(rc, rm, !listOnly, CreateFile).Result; + TestPrep.CompareTestResults(results1[0], results1[1], listOnly); + + void CreateFile() + { + PrepMoveFiles(); + Directory.CreateDirectory(TestPrep.DestDirPath); + string fn = "1024_Bytes.txt"; + string dest = Path.Combine(TestPrep.DestDirPath, fn); + if (!File.Exists(dest)) + File.Copy(Path.Combine(TestPrep.SourceDirPath, fn), dest); + } + } + + [TestMethod] + [Timeout(10000)] + // purge all + [DataRow(0, true, Mov_)] + [DataRow(0, true, Move)] + [DataRow(0, true, Mov_ | CopyActionFlags.CopySubdirectories)] + [DataRow(0, true, Move | CopyActionFlags.CopySubdirectories)] + [DataRow(0, true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + [DataRow(0, true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + [DataRow(0, true, Mov_, LoggingFlags.ReportExtraFiles)] + [DataRow(0, true, Mov_ | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] + [DataRow(0, true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] + // purge depth 1 + [DataRow(1, true, Mov_)] + [DataRow(1, true, Move)] + [DataRow(1, true, Mov_ | CopyActionFlags.CopySubdirectories)] + [DataRow(1, true, Move | CopyActionFlags.CopySubdirectories)] + [DataRow(1, true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + [DataRow(1, true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + // purge depth 2 + [DataRow(2, true, Mov_)] + [DataRow(2, false, Move)] + [DataRow(2, true, Mov_ | CopyActionFlags.CopySubdirectories)] + [DataRow(2, true, Move | CopyActionFlags.CopySubdirectories)] + [DataRow(2, false, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + [DataRow(2, false, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + [DataRow(2, true, Mov_, LoggingFlags.ReportExtraFiles)] + [DataRow(2, true, Mov_ | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] + [DataRow(2, true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] + public void Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + { + LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; + GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); + cmd.LoggingOptions.ListOnly = listOnly; + cmd.CopyOptions.Depth = depth; + RunPurge(cmd, mover); + } + + [TestMethod] + [Timeout(10000)] + [DataRow(true, Mov_)] + [DataRow(false, Move)] + [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories)] + [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] + [DataRow(false, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + [DataRow(false, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + public void Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) + { + GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); + cmd.LoggingOptions.ListOnly = listOnly; + cmd.SelectionOptions.ExcludedFiles.Add("*0*_Bytes.txt"); + RunPurge(cmd, mover); + } + + [TestMethod] + [Timeout(10000)] + [DataRow(true, Mov_)] + [DataRow(true, Move)] + [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories)] + [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] + [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + public void Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) + { + GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); + cmd.LoggingOptions.ListOnly = listOnly; + cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder1"); // Top level empty + cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder4"); // Bottom level empty + cmd.SelectionOptions.ExcludedDirectories.Add("SubFolder_2a"); // folder with contents + RunPurge(cmd, mover); + } + + [TestMethod] + [Timeout(10000)] + [DataRow(true, Mov_)] + [DataRow(true, Move)] + [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories)] + [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] + [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] + [DataRow(true, Mov_, LoggingFlags.ReportExtraFiles)] + [DataRow(true, Move, LoggingFlags.ReportExtraFiles)] + [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] + [DataRow(true, Move | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] + [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] + [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] + public void Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + { + LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; + GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); + cmd.LoggingOptions.ListOnly = listOnly; + cmd.CopyOptions.FileFilter = new string[] { "*0*_Bytes.txt" }; + RunPurge(cmd, mover); + } + + private void RunPurge(RoboCommand cmd, FactoryCommand mover) + { + //if (Test_Setup.IsRunningOnAppVeyor()) return; + var results = TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, CreateFilesToPurge).Result; + TestPrep.CompareTestResults(results[0], results[1], cmd.LoggingOptions.ListOnly); + } + + [TestMethod] + [Timeout(10000)] + public void CreateFilesToPurge() + { + PrepMoveFiles(); + RoboCommand prep = new RoboCommand(); + prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_1"); + prep.CopyOptions.Destination = Path.Combine(Test_Setup.TestDestination, "SubFolder_3"); + prep.CopyOptions.ApplyActionFlags(CopyActionFlags.CopySubdirectoriesIncludingEmpty); + Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder1", "EmptyFolder2")); + prep.Start().Wait(); + prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_2"); + prep.CopyOptions.Destination = Path.Combine(prep.CopyOptions.Destination, "SubFolder_2a"); + prep.Start().Wait(); + Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder3", "EmptyFolder4")); + } + } +} +#endif \ No newline at end of file diff --git a/RoboSharp.Extensions/FactoryCommand.cs b/RoboSharp.Extensions/FactoryCommand.cs new file mode 100644 index 00000000..3878bc4a --- /dev/null +++ b/RoboSharp.Extensions/FactoryCommand.cs @@ -0,0 +1,579 @@ + +using RoboSharp.EventArgObjects; +using RoboSharp.Extensions.Helpers; +using RoboSharp.Extensions.Options; +using RoboSharp.Interfaces; +using RoboSharp.Results; +using System; +using System.CodeDom; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Design; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace RoboSharp.Extensions +{ + /// + /// This relies on an to generate the objects used to manage the copy operations. + ///
This class should allow use of this library in non-windows environments. + ///
+ public class FactoryCommand : IRoboCommand, INotifyPropertyChanged + { + internal static void ThrowUnsupportedFrameworkException() + { +#if !(NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER || NET8_0_OR_GREATER) + throw new System.NotSupportedException("This process relies on IAsyncEnumerable, which is not present for this framework."); +#endif + } + + /// + /// Create a new + /// + /// + /// + /// The used to validate the robocommand prior to running. + ///
Default uses + /// + /// + /// Not Available in .Net Framework or .NetStandard2.0 + public FactoryCommand(IFileCopierFactory fileCopierFactory, IAuthenticator? authenticator = null) + { + ThrowUnsupportedFrameworkException(); + copierFactory = fileCopierFactory ?? throw new ArgumentNullException(nameof(fileCopierFactory)); + this.authenticator = authenticator ?? SourceAndDestinationAuthenticator.Instance; + } + + private readonly IFileCopierFactory copierFactory; + private readonly IAuthenticator authenticator; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public event RoboCommand.FileProcessedHandler? OnFileProcessed; + public event RoboCommand.CommandErrorHandler? OnCommandError; + public event RoboCommand.ErrorHandler? OnError; + public event RoboCommand.CommandCompletedHandler? OnCommandCompleted; + public event RoboCommand.CopyProgressHandler? OnCopyProgressChanged; + public event RoboCommand.ProgressUpdaterCreatedHandler? OnProgressEstimatorCreated; + public event UnhandledExceptionEventHandler? TaskFaulted; + public event PropertyChangedEventHandler? PropertyChanged; + + + private void SetProperty(ref T field, T value, string name) + { + System.Diagnostics.Debug.Assert(string.IsNullOrWhiteSpace(name) == false, "name parameter has no value"); + if ((field is not null && field.Equals(value) == false) || (field is null && value is not null)) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + System.Diagnostics.Debug.Assert(field?.Equals(value) ?? (value is null && field is null), "FactoryCommand.SetProperty failed to update field.", "Field {0} value was not updated to value [{1}]'", name, value); + } + + private string name = string.Empty; + private bool isPaused = false, isRunning = false, isScheduled = false, isCancelled = false, stopIfDisposing; + private CopyOptions _CopyOptions = new(); + private SelectionOptions _SelectionOptions = new(); + private RetryOptions _RetryOptions = new(); + private LoggingOptions _LoggingOptions = new(); + private JobOptions _JobOptions = new(); + private RoboSharpConfiguration _Configuration = new(); + private IProgressEstimator? progressEstimator; + private CancellationTokenSource? _CancellationTokenSource; + private SemaphoreSlim _startLock = new SemaphoreSlim(1, 1); + private RoboCopyResults? _lastResults; + + public string Name { get => name; private set => SetProperty(ref name, value, nameof(Name)); } + public bool IsPaused { get => isPaused; private set => SetProperty(ref isPaused, value, nameof(IsPaused)); } + public bool IsRunning { get => isRunning; private set => SetProperty(ref isRunning, value, nameof(IsRunning)); } + public bool IsScheduled { get => isScheduled; private set => SetProperty(ref isScheduled, value, nameof(IsScheduled)); } + public bool IsCancelled { get => isCancelled; private set => SetProperty(ref isCancelled, value, nameof(IsCancelled)); } + public bool StopIfDisposing { get => stopIfDisposing; private set => SetProperty(ref stopIfDisposing, value, nameof(StopIfDisposing)); } + public IProgressEstimator? IProgressEstimator { get => progressEstimator; private set => SetProperty(ref progressEstimator, value, nameof(IProgressEstimator)); } + public string CommandOptions => GenerateParameters(); + public CopyOptions CopyOptions { get => _CopyOptions; set => SetProperty(ref _CopyOptions, value, nameof(CopyOptions)); } + public SelectionOptions SelectionOptions { get => _SelectionOptions; set => SetProperty(ref _SelectionOptions, value, nameof(SelectionOptions)); } + public RetryOptions RetryOptions { get => _RetryOptions; set => SetProperty(ref _RetryOptions, value, nameof(RetryOptions)); } + public LoggingOptions LoggingOptions { get => _LoggingOptions; set => SetProperty(ref _LoggingOptions, value, nameof(LoggingOptions)); } + public JobOptions JobOptions { get => _JobOptions; set => SetProperty(ref _JobOptions, value, nameof(JobOptions)); } + public RoboSharpConfiguration Configuration { get => _Configuration; set => SetProperty(ref _Configuration, value, nameof(Configuration)); } + + + public void Pause() + { + if (IsRunning) + { + IsPaused = true; + } + } + + public void Resume() + { + if (IsPaused) + { + IsPaused = false; + } + } + + public void Stop() + { + _CancellationTokenSource?.Cancel(); + IsCancelled = _CancellationTokenSource?.IsCancellationRequested ?? false; + } + + public Task Start(string domain = "", string username = "", string password = "") + { + return Run(domain, username, password); + } + + public Task Start_ListOnly(string domain = "", string username = "", string password = "") + { + return Run(domain, username, password, PreRunListOnlyAction, PostRunListOnlyAction); + } + + public async Task StartAsync(string domain = "", string username = "", string password = "") + { + await Run(domain, username, password); + return GetResults(); + } + + public async Task StartAsync_ListOnly(string domain = "", string username = "", string password = "") + { + await Run(domain, username, password, PreRunListOnlyAction, PostRunListOnlyAction); + return GetResults(); + } + + public RoboCopyResults? GetResults() + { + return _lastResults; + } + + public void Dispose() + { + this._CancellationTokenSource?.Cancel(); + } + + + /// + /// Generate the Parameters and Switches to execute RoboCopy with based on the configured settings + /// + /// + private string GenerateParameters() + { + var parsedCopyOptions = CopyOptions.Parse(); + var parsedSelectionOptions = SelectionOptions.Parse(); + var parsedRetryOptions = RetryOptions.ToString(); + var parsedLoggingOptions = LoggingOptions.ToString(); + var parsedJobOptions = JobOptions.ToString(); + //var systemOptions = " /V /R:0 /FP /BYTES /W:0 /NJH /NJS"; + return string.Format("{0}{1}{2}{3}{4}", parsedCopyOptions, parsedSelectionOptions, + parsedRetryOptions, parsedLoggingOptions, parsedJobOptions); + } + + /// + public override string ToString() + { + return GenerateParameters(); + } + + /// + /// Combine this object's options with that of some JobFile + /// + /// + public void MergeJobFile(JobFile jobFile) + { + Name = string.IsNullOrWhiteSpace(Name) ? jobFile.Name ?? "" : Name; + CopyOptions.Merge(jobFile.CopyOptions); + LoggingOptions.Merge(jobFile.LoggingOptions); + RetryOptions.Merge(jobFile.RetryOptions); + SelectionOptions.Merge(jobFile.SelectionOptions); + JobOptions.Merge(((IRoboCommand)jobFile).JobOptions); + //this.StopIfDisposing |= ((IRoboCommand)jobFile).StopIfDisposing; + } + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + + void PreRunListOnlyAction() + { + LoggingOptions.ListOnly = true; + } + void PostRunListOnlyAction() + { + LoggingOptions.ListOnly = false; + } + + + +#if !(NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER || NET8_0_OR_GREATER) + + private Task Run(string domain, string username, string password, Action? preRunAction = null, Action? postRunAction = null) + { + ThrowUnsupportedFrameworkException(); + return Task.CompletedTask; + } + +#else + + private Regex[] GetFileExclusionRegex() => excludedFiledRegex??= SelectionOptions.GetExcludedFileRegex(); + private Regex[]? excludedFiledRegex; + + private Regex[] GetFileFilterRegex() => fileFilterRegex ??= CopyOptions.GetFileFilterRegex(); + private Regex[]? fileFilterRegex; + + private DirectoryRegex[] GetDirectoryRegexes() => directoryRegexes ??= SelectionOptions.GetExcludedDirectoryRegex(); + private DirectoryRegex[]? directoryRegexes; + + private void EvaluateFilePair(IFileCopier pair) => pair.ProcessFilePairAgainstCommandOptions(this, GetFileFilterRegex(), GetFileExclusionRegex()); + private void EvaluateDirPair(DirectoryPair pair) => pair.EvaluateDirectoryPair(this, GetDirectoryRegexes()); + + private void RaiseProgressUpdated(object? sender, CopyProgressEventArgs e) => OnCopyProgressChanged?.Invoke(this, e); + + private async Task Run(string domain, string username, string password, Action? preRunAction = null, Action? postRunAction = null) + { + await _startLock.WaitAsync(CancellationToken.None); + if (IsRunning) + { + _startLock.Release(); + throw new InvalidOperationException($"{nameof(FactoryCommand)} is already running."); + } + IsRunning = true; + IsPaused = false; + IsCancelled = false; + + // Sanity Checks + var authResult = authenticator.Authenticate(this, domain, username, password); + if (!authResult.Success) + { + OnCommandError?.Invoke(this, authResult.CommandErrorArgs); + isRunning = false; + return; + } + + _CancellationTokenSource = new CancellationTokenSource(); + var token = _CancellationTokenSource.Token; + + try + { + // Pre-Run Action + preRunAction?.Invoke(); + await RunAsync(token); + } + finally + { + postRunAction?.Invoke(); + } + } + + /* + * Code below this point was generated with the assistance of Claude.ai for the actual robocopy implementation + * Modified as needed + */ + + /// + /// Core execution loop. Two-pass design mirrors Robocopy: + /// Pass 1 – directory scan: feed ProgressEstimator so the UI has estimates upfront. + /// Pass 2 – directory process: evaluate, copy/skip/purge, fire IRoboCommand events, + /// record every outcome in ResultsBuilder. + /// + private async Task RunAsync(CancellationToken cancellationToken) + { + // ── Infrastructure setup ────────────────────────────────────────────────── + + + var progressReporter = new ProgressEstimator(this); // live IStatistic feeds for the UI + var resultsBuilder = new ResultsBuilder(this); // tracks counts/bytes per category + + // Fire OnProgressEstimatorCreated so subscribers (e.g. a progress bar) can + // attach to the estimator's IStatistic change events before work begins. + this.IProgressEstimator = progressReporter; + OnProgressEstimatorCreated?.Invoke(this, new ProgressEstimatorCreatedEventArgs(progressReporter)); + + bool includeEmpty = this.CopyOptions.CopySubdirectoriesIncludingEmpty || CopyOptions.Mirror; + bool recurse = this.CopyOptions.CopySubdirectories || this.CopyOptions.CopySubdirectoriesIncludingEmpty || CopyOptions.Mirror; + int maxDepth = !recurse ? 1 : CopyOptions.Depth == 0 ? int.MaxValue : CopyOptions.Depth; + var rootPair = new DirectoryPair(this.CopyOptions.Source, this.CopyOptions.Destination); + bool listOnly = LoggingOptions.ListOnly; + bool touchFiles = CopyOptions.CreateDirectoryAndFileTree; + + + SemaphoreSlim multiThreadedController = new SemaphoreSlim(CopyOptions.MultiThreadedCopiesCount >= 128 ? 128 : CopyOptions.MultiThreadedCopiesCount <= 1 ? 1 : CopyOptions.MultiThreadedCopiesCount); + Dictionary infoDict = new(); + ConcurrentDictionary runningTasks = new(); + + try + { + + // ── Pass 1: pre-scan to seed ProgressEstimator ──────────────────────────── + // Robocopy reports totals before starting transfers; we replicate that here + // so ProgressEstimator can give accurate percentage estimates from the start. + + await foreach (var dirPair in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) + { + // Tell the estimator a directory exists on the source side + EvaluateDirPair(dirPair); + progressReporter.AddDir(dirPair.ProcessedFileInfo); + + infoDict[dirPair.Source.FullName] = dirPair.ProcessedFileInfo; + + await foreach (IFileCopier copier in CreateFileCopiers(dirPair, cancellationToken)) + { + dirPair.ProcessedFileInfo.Size++; + } + } + + // ── Pass 2: process each directory ─────────────────────────────────────── + await foreach (var dirPair in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (infoDict.TryGetValue(dirPair.Source.FullName, out var pInfo)) + { + dirPair.ProcessedFileInfo = pInfo; + infoDict.Remove(dirPair.Source.FullName); // key will never be read again + } + else + { + EvaluateDirPair(dirPair); + } + + progressReporter.AddDir(dirPair.ProcessedFileInfo); + resultsBuilder.AddDir(dirPair.ProcessedFileInfo); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(dirPair.ProcessedFileInfo)); + + if (includeEmpty) + dirPair.Destination.Create(); + + // ── 2a. Source files ────────────────────────────────────────────────── + + await foreach (IFileCopier copier in CreateFileCopiers(dirPair, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Evaluate populates copier.ProcessedFileInfo (FileClass, Size, Name) + // AND sets ShouldCopy / ShouldPurge based on this IRoboCommand's options. + EvaluateFilePair(copier); + + ProcessedFileInfo fileInfo = copier.ProcessedFileInfo; + + if (copier.ShouldCopy) + { + if (listOnly) + { + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); + progressReporter.AddFileCopied(fileInfo); + resultsBuilder.AddFileCopied(fileInfo); + } + else if (touchFiles) + { + dirPair.Destination.Create(); + if (copier.Destination.Exists is false) + copier.Destination.Create(); + + progressReporter.AddFileCopied(fileInfo); + resultsBuilder.AddFileCopied(fileInfo); + } + else + { + await multiThreadedController.WaitAsync(cancellationToken); + + // Announce the file before the transfer (mirrors Robocopy's pre-copy log line) + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); + runningTasks[copier] = Task.Run(async () => + { + try + { + Directory.CreateDirectory(dirPair.Destination.FullName); + copier.ProgressUpdated += RaiseProgressUpdated; + if (CopyOptions.MoveFiles || CopyOptions.MoveFilesAndDirectories) + await copier.MoveAsync(true, cancellationToken).ConfigureAwait(false); + else + await copier.CopyAsync(true, cancellationToken).ConfigureAwait(false); + + progressReporter.AddFileCopied(fileInfo); + resultsBuilder.AddFileCopied(fileInfo); + } + catch (OperationCanceledException) + { + throw; // let cancellation propagate cleanly + } + catch (Exception ex) + { + resultsBuilder.AddFileFailed(fileInfo); + OnError?.Invoke(this, new ErrorEventArgs(ex, copier.Destination.FullName, DateTime.Now)); + } + finally + { + copier.ProgressUpdated -= RaiseProgressUpdated; + runningTasks.TryRemove(copier, out _); + multiThreadedController.Release(); + } + }, cancellationToken); + } + } + else + { + // File was evaluated but not copied (skipped/extra/same/newer/older). + // Still report it so consumers see the full picture. + progressReporter.AddFileSkipped(fileInfo); + resultsBuilder.AddFileSkipped(fileInfo); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); + } + } + + // ── 2b. Purge candidates (destination-only files) ──────────────────── + + await foreach (IFileCopier purgeCopier in CreatePurgeCandidates(dirPair, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + EvaluateFilePair(purgeCopier); + ProcessedFileInfo purgeInfo = purgeCopier.ProcessedFileInfo; + + if (purgeCopier.ShouldPurge) + { + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(purgeInfo)); + + try + { + purgeCopier.Destination.Delete(); + progressReporter.AddFileExtra(purgeInfo); + resultsBuilder.AddFilePurged(purgeInfo); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + resultsBuilder.AddFileFailed(purgeInfo); + OnCommandError?.Invoke(this, new CommandErrorEventArgs(ex.Message, ex)); + } + } + else + { + // Extra file is present but purge is disabled — treat as skipped/extra + progressReporter.AddFileExtra(purgeInfo); + resultsBuilder.AddFileExtra(purgeInfo); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(purgeInfo)); + } + } + } + + if (runningTasks.IsEmpty == false) + await Task.WhenAll(runningTasks.Values); + + // ── Completion ─────────────────────────────────────────────────────────── + RoboCopyResults results = resultsBuilder.GetResults(); + _lastResults = results; + OnCommandCompleted?.Invoke(this, new RoboCommandCompletedEventArgs(results)); + } + catch + { + _lastResults = resultsBuilder.GetResults(); + throw; + } + } + + // ── Helpers ────────────────────────────────────────────────────────────────── + + /// + /// Yields the root pair and (if recurse is true) all sub-directory pairs, + /// mirroring Robocopy's directory tree walk. + /// + private async IAsyncEnumerable EnumerateDirectoryPairsAsync(DirectoryPair root, int currentDepth, int maxDepth, [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return root; + + if (currentDepth >= maxDepth) + yield break; + + // Offload the synchronous Directory.EnumerateDirectories call onto the thread pool + // so the caller's await loop stays non-blocking. + IEnumerable subDirs = await Task.Run(() => Directory.EnumerateDirectories(root.Source.FullName, "*", SearchOption.TopDirectoryOnly), cancellationToken) + .ConfigureAwait(false); + + foreach (string sourceSubDir in subDirs) + { + while (IsPaused && !cancellationToken.IsCancellationRequested) + await Task.Delay(50, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + string relative = Path.GetRelativePath(root.Source.FullName, sourceSubDir); + string destSubDir = Path.Combine(root.Destination.FullName, relative); + + // Adjust to however your IDirectoryPair is constructed + var subPair = new DirectoryPair(sourceSubDir, destSubDir); + + await foreach (var child in EnumerateDirectoryPairsAsync(subPair, currentDepth + 1, maxDepth, cancellationToken)) + { + yield return child; + } + } + } + + /// + /// Creates instances for every source file in + /// using each factory in _copierFactories. + /// The factory decides the copier implementation; we just enumerate source files + /// and hand each pair to the factory. + /// + private async IAsyncEnumerable CreateFileCopiers(IDirectoryPair dirPair, [EnumeratorCancellation] CancellationToken cancellationToken) + { + IEnumerable sourceFiles = await Task.Run(() => dirPair.Source.EnumerateFiles("*", SearchOption.TopDirectoryOnly), cancellationToken) + .ConfigureAwait(false); + + foreach (FileInfo sourceFile in sourceFiles) + { + while (IsPaused && !cancellationToken.IsCancellationRequested) + await Task.Delay(50, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + // Map to the corresponding destination FileInfo + string destPath = Path.Combine(dirPair.Destination.FullName, sourceFile.Name); + var destFile = new FileInfo(destPath); + + yield return copierFactory.Create(sourceFile, destFile, dirPair); + } + } + + /// + /// Creates purge-candidate instances for files that + /// exist in the destination but not the source (i.e. "extra" files). + /// + private async IAsyncEnumerable CreatePurgeCandidates(IDirectoryPair dirPair, [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (!Directory.Exists(dirPair.Destination.FullName)) + yield break; + + IEnumerable destFiles = await Task.Run(() => dirPair.Destination.EnumerateFiles("*", SearchOption.TopDirectoryOnly), cancellationToken) + .ConfigureAwait(false); + + foreach (FileInfo destFile in destFiles) + { + while (IsPaused && !cancellationToken.IsCancellationRequested) + await Task.Delay(50, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + string sourcePath = Path.Combine(dirPair.Source.FullName, destFile.Name); + + if (File.Exists(sourcePath)) + continue; + + var sourceFile = new FileInfo(sourcePath); + yield return copierFactory.Create(sourceFile, destFile, dirPair); + } + } +#endif + } +} diff --git a/RoboSharp.Extensions/FactoryCommandFactory.cs b/RoboSharp.Extensions/FactoryCommandFactory.cs new file mode 100644 index 00000000..0d5092eb --- /dev/null +++ b/RoboSharp.Extensions/FactoryCommandFactory.cs @@ -0,0 +1,70 @@ +using RoboSharp.Interfaces; +using System; + +#nullable enable + +namespace RoboSharp.Extensions +{ + /// + /// An that creates objects that will use some to determine how the copy operation is actually performed. + ///
This class should allow use of this library in non-windows environments. + ///
+ public class FactoryCommandFactory : IRoboCommandFactory + { + /// + /// Create a new to produce objects + /// + /// The factory to use when a copy or move operation is required + /// + /// The used to validate the robocommand prior to running. + ///
Default uses + /// + /// + /// Applies to .NetFramework and .NetStandard2.0 + public FactoryCommandFactory(IFileCopierFactory fileCopierFactory, IAuthenticator? authenticator = null) + { + FactoryCommand.ThrowUnsupportedFrameworkException(); + _fileCopierFactory = fileCopierFactory ?? throw new ArgumentNullException(nameof(fileCopierFactory)); + _authenticator = authenticator ?? SourceAndDestinationAuthenticator.Instance; + } + + private readonly IFileCopierFactory _fileCopierFactory; + private readonly IAuthenticator _authenticator; + + /// + public IRoboCommand GetRoboCommand() + { + return new FactoryCommand(_fileCopierFactory, _authenticator); + } + + /// + public IRoboCommand GetRoboCommand(string source, string destination) + { + var cmd = new FactoryCommand(_fileCopierFactory, _authenticator); + cmd.CopyOptions.Source = source; + cmd.CopyOptions.Destination = destination; + return cmd; + } + + /// + public IRoboCommand GetRoboCommand(string source, string destination, CopyActionFlags copyActionFlags) + { + var cmd = new FactoryCommand(_fileCopierFactory, _authenticator); + cmd.CopyOptions.Source = source; + cmd.CopyOptions.Destination = destination; + cmd.CopyOptions.ApplyActionFlags(copyActionFlags); + return cmd; + } + + /// + public IRoboCommand GetRoboCommand(string source, string destination, CopyActionFlags copyActionFlags, SelectionFlags selectionFlags) + { + var cmd = new FactoryCommand(_fileCopierFactory, _authenticator); + cmd.CopyOptions.Source = source; + cmd.CopyOptions.Destination = destination; + cmd.CopyOptions.ApplyActionFlags(copyActionFlags); + cmd.SelectionOptions.ApplySelectionFlags(selectionFlags); + return cmd; + } + } +} \ No newline at end of file diff --git a/RoboSharp.Extensions/Helpers/IDirectoryPairExtensions.cs b/RoboSharp.Extensions/Helpers/IDirectoryPairExtensions.cs index 626b483f..700853b7 100644 --- a/RoboSharp.Extensions/Helpers/IDirectoryPairExtensions.cs +++ b/RoboSharp.Extensions/Helpers/IDirectoryPairExtensions.cs @@ -1,9 +1,12 @@ -using System; +using RoboSharp.Extensions.Options; +using RoboSharp.Interfaces; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace RoboSharp.Extensions.Helpers @@ -73,6 +76,49 @@ public static bool TrySetSizeAndPath(this IProcessedDirectoryPair pair, bool pri return false; } + /// + /// Setup the and determine if it should be recursed into for the purposes of copying or moving. + ///
note : Purging is ignored here, so 'Extra' pairs will always return false. + ///
+ /// The directory pair to evaluate + /// the associated IRoboCommand + /// provide a cached object that stores the result of : + /// Immediately get the count of files - may be expensive!!! + /// + /// true if the directory should be processed further for COPYING, otherwise false. + ///
Note: purging/mirroing is ignored for this evaluation. + ///
+ public static bool EvaluateDirectoryPair(this IProcessedDirectoryPair pair, IRoboCommand command, IEnumerable directoryExclusionRegex, bool getFileCount = false) + { + var info = pair.ProcessedFileInfo ??= new ProcessedFileInfo(); + + if (pair.IsExtra()) + { + info.Name = pair.Destination.FullName; + info.Size = getFileCount ? pair.Destination.GetFiles().Length : 0; + info.SetDirectoryClass(ProcessedDirectoryFlag.ExtraDir, command.Configuration); + return false; + } + + info.Name = pair.Source.FullName; + info.Size = getFileCount ? (pair.Source.Exists ? pair.Source.GetFiles().Length : 0) : 0; + + if (command.SelectionOptions.ShouldExcludeDirectoryName(pair, directoryExclusionRegex)) + { + info.SetDirectoryClass(ProcessedDirectoryFlag.Exclusion, command.Configuration); + return false; + } + + if (pair.IsLonely()) + { + info.SetDirectoryClass(ProcessedDirectoryFlag.NewDir, command.Configuration); + return true; + } + + info.SetDirectoryClass(ProcessedDirectoryFlag.ExistingDir, command.Configuration); + return true; + } + /// /// Refreshes both the and objects /// diff --git a/RoboSharp.Extensions/Helpers/IFilePairExtensions.cs b/RoboSharp.Extensions/Helpers/IFilePairExtensions.cs index 14a0a380..30b5d07a 100644 --- a/RoboSharp.Extensions/Helpers/IFilePairExtensions.cs +++ b/RoboSharp.Extensions/Helpers/IFilePairExtensions.cs @@ -1,9 +1,12 @@ -using System; +using RoboSharp.Extensions.Options; +using RoboSharp.Interfaces; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace RoboSharp.Extensions.Helpers @@ -13,6 +16,121 @@ namespace RoboSharp.Extensions.Helpers /// public static class IFilePairExtensions { + /// + /// Evaluate a against the options to determine if it should be copied or not. + /// + /// the file pair to evaluate + /// The associated command + /// + /// + /// + public static bool ProcessFilePairAgainstCommandOptions(this IFileCopier pair, IRoboCommand command, IEnumerable copyOptions_FileNameNameInclusions, IEnumerable SelectionOptions_FileNameNameExclusions) + { + var sOptions = command.SelectionOptions; + pair.ShouldCopy = false; + + // Extra + if (pair.IsExtra()) + { + pair.ProcessedFileInfo = new ProcessedFileInfo(pair.Destination, command, ProcessedFileFlag.ExtraFile); + _ = command.ShouldPurge(pair); // process for purging + return false; + } + + pair.ShouldPurge = false; + ProcessedFileInfo pInfo = pair.ProcessedFileInfo ??= new ProcessedFileInfo(pair.Source, command, ProcessedFileFlag.None); + + // lonely files + if (sOptions.ShouldExcludeLonely(pair)) + { + pInfo.SetFileClass(ProcessedFileFlag.NewFile, command.Configuration); + return false; + } + + // file age + if (sOptions.ShouldExcludeMaxFileAge(pair)) + { + pInfo.SetFileClass(ProcessedFileFlag.MaxAgeSizeExclusion, command.Configuration); + return false; + } + if (sOptions.ShouldExcludeMinFileAge(pair)) + { + pInfo.SetFileClass(ProcessedFileFlag.MinAgeSizeExclusion, command.Configuration); + return false; + } + + // file size + if (sOptions.ShouldExcludeMaxFileSize(pair)) + { + pInfo.SetFileClass(ProcessedFileFlag.MaxFileSizeExclusion, command.Configuration); + return false; + } + if (sOptions.ShouldExcludeMinFileSize(pair)) + { + pInfo.SetFileClass(ProcessedFileFlag.MinFileSizeExclusion, command.Configuration); + return false; + } + + // older / newer + if (sOptions.ShouldExcludeNewer(pair)) + { + pInfo.SetFileClass(ProcessedFileFlag.NewerFile, command.Configuration); + return false; + } + if (sOptions.ShouldExcludeOlder(pair)) + { + pInfo.SetFileClass(ProcessedFileFlag.OlderFile, command.Configuration); + return false; + } + + // access date + if (sOptions.ShouldExcludeMaxLastAccessDate(pair)) + { + pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); + return false; + } + if (sOptions.ShouldExcludeMinLastAccessDate(pair)) + { + pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); + return false; + } + + if (sOptions.ShouldIncludeAttributes(pair) == false) + { + pInfo.SetFileClass(ProcessedFileFlag.AttribExclusion, command.Configuration); + return false; + } + + // potentially expensive regex tests + if (!Options.CopyExtensions.ShouldIncludeFileName(command.CopyOptions, pair.Source, copyOptions_FileNameNameInclusions)) + { + pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); + return false; + } + + if (Options.SelectionExtensions.ShouldExcludeFileName(command.SelectionOptions, pair.Source, SelectionOptions_FileNameNameExclusions)) + { + pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); + return false; + } + + if (pair.IsSameDate()) + { + pInfo.SetFileClass(ProcessedFileFlag.SameFile, command.Configuration); + pair.ShouldCopy = command.SelectionOptions.IncludeSame; + return pair.ShouldCopy; + } + + pair.ShouldCopy = true; + ProcessedFileFlag flag = pair.IsLonely() ? ProcessedFileFlag.NewFile + : pair.IsSourceNewer() ? ProcessedFileFlag.NewerFile + : pair.IsDestinationNewer() ? ProcessedFileFlag.OlderFile + : pair.IsSameDate() ? ProcessedFileFlag.SameFile : ProcessedFileFlag.TweakedInclusion; + + pInfo.SetFileClass(flag, command.Configuration); + return true; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool FileDoesntExist(string destination) => !File.Exists(destination); diff --git a/RoboSharp.Extensions/Helpers/ResultsBuilder.cs b/RoboSharp.Extensions/Helpers/ResultsBuilder.cs index 21e4d574..1fea079c 100644 --- a/RoboSharp.Extensions/Helpers/ResultsBuilder.cs +++ b/RoboSharp.Extensions/Helpers/ResultsBuilder.cs @@ -219,7 +219,8 @@ public virtual void AddFilePurged(ProcessedFileInfo file) public virtual void AddFileSkipped(ProcessedFileInfo file) { ProgressEstimator.AddFileSkipped(file); - LogFileInfo(file); + if (Command.LoggingOptions.ReportExtraFiles) + LogFileInfo(file); } /// diff --git a/RoboSharp.Extensions/IAuthenticator.cs b/RoboSharp.Extensions/IAuthenticator.cs new file mode 100644 index 00000000..89fb2c03 --- /dev/null +++ b/RoboSharp.Extensions/IAuthenticator.cs @@ -0,0 +1,62 @@ +using RoboSharp.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RoboSharp.Extensions +{ + /// + /// Interface for authenticating an is valid to continue + /// + public interface IAuthenticator + { + /// + /// Validate if the robocommand can continue. + /// + /// + /// + /// + /// + /// + AuthenticationResult Authenticate(IRoboCommand command, string domain, string username, string password); + } + + /// + /// The Default provider. + /// Uses + /// + public sealed class DefaultAuthenticator : IAuthenticator + { + /// + /// A thread-safe singleton that can be used + /// + public static IAuthenticator Instance => instance ??= new(); + private static DefaultAuthenticator instance = null; + + /// + public AuthenticationResult Authenticate(IRoboCommand command, string domain, string username, string password) + { + return Authentication.AuthenticateSourceAndDestination(command, domain, username, password); + } + } + + /// + /// An that only checks that the source and destination directories are accessible + /// + public sealed class SourceAndDestinationAuthenticator : IAuthenticator + { + /// + /// A thread-safe singleton that can be used + /// + public static IAuthenticator Instance => instance ??= new(); + private static SourceAndDestinationAuthenticator instance = null; + + /// + public AuthenticationResult Authenticate(IRoboCommand command, string domain, string username, string password) + { + return Authentication.CheckSourceAndDestinationDirectories(command); + } + } +} diff --git a/RoboSharp/Authentication.cs b/RoboSharp/Authentication.cs index 304bf770..8aa3f30c 100644 --- a/RoboSharp/Authentication.cs +++ b/RoboSharp/Authentication.cs @@ -121,7 +121,7 @@ private static AuthenticationResult Authenticate( ///
/// /// if both the source and destination are accessible, otherwise - private static AuthenticationResult CheckSourceAndDestinationDirectories(IRoboCommand command) + public static AuthenticationResult CheckSourceAndDestinationDirectories(IRoboCommand command) { var source = CheckSourceDirectory(command); if (source.Success) @@ -134,7 +134,7 @@ private static AuthenticationResult CheckSourceAndDestinationDirectories(IRoboCo /// Check that the source directory exists. /// /// if the source is accessible, otherwise - private static AuthenticationResult CheckSourceDirectory(IRoboCommand command) + public static AuthenticationResult CheckSourceDirectory(IRoboCommand command) { const string SourceMissing = "The Source directory does not exist."; @@ -152,7 +152,7 @@ private static AuthenticationResult CheckSourceDirectory(IRoboCommand command) /// If not list only, verify that the destination drive has write access. /// /// if the destination is accessible, otherwise - private static AuthenticationResult CheckDestinationDirectory(IRoboCommand command) + public static AuthenticationResult CheckDestinationDirectory(IRoboCommand command) { //Check that the Destination Drive is accessible instead [fixes #106] try diff --git a/RoboSharp/DefaultConfigurations/RoboSharpConfig_EN.cs b/RoboSharp/DefaultConfigurations/RoboSharpConfig_EN.cs index dc887ff1..cff238e3 100644 --- a/RoboSharp/DefaultConfigurations/RoboSharpConfig_EN.cs +++ b/RoboSharp/DefaultConfigurations/RoboSharpConfig_EN.cs @@ -33,7 +33,7 @@ public RoboSharpConfig_EN() : base() LogParsing_NewDir = "New Dir"; LogParsing_ExtraDir = "*EXTRA Dir"; - LogParsing_ExistingDir = "Existing Dir"; + LogParsing_ExistingDir = ""; LogParsing_DirectoryExclusion = "named"; } diff --git a/RoboSharp/ProcessedFileInfo.cs b/RoboSharp/ProcessedFileInfo.cs index ae4520c2..337cf980 100644 --- a/RoboSharp/ProcessedFileInfo.cs +++ b/RoboSharp/ProcessedFileInfo.cs @@ -59,7 +59,7 @@ public ProcessedFileInfo(FileInfo file, IRoboCommand command, ProcessedFileFlag FileClassType = FileClassType.File; FileClass = command.Configuration.GetFileClass(status); Name = command.LoggingOptions.IncludeFullPathNames ? file.FullName : file.Name; - Size = file.Length; + Size = file.Exists ? file.Length : 0; } /// @@ -188,9 +188,10 @@ public string ToStringFailed(IRoboCommand command, Exception ex = null, DateTime private string DirInfoToString(bool includeSize) { if (includeSize) - return $"\t{FileClass,-10} \t{Name}"; - else + { return $"\t{FileClass,-10}{Size,12}\t{Name}"; + } + return $"\t{FileClass,-10} \t{Name}"; } /// diff --git a/RoboSharp/RoboSharpConfiguration.cs b/RoboSharp/RoboSharpConfiguration.cs index f2058eb3..46a79b1a 100644 --- a/RoboSharp/RoboSharpConfiguration.cs +++ b/RoboSharp/RoboSharpConfiguration.cs @@ -326,7 +326,7 @@ public string LogParsing_ExtraDir /// public string LogParsing_ExistingDir { - get { return existingDirToken ?? GetDefaultConfiguration().existingDirToken ?? "Existing Dir"; } + get { return existingDirToken ?? GetDefaultConfiguration().existingDirToken ?? ""; } set { existingDirToken = value; } } private string existingDirToken; From be31e3569d38db94438b4416de0ef339cafbeaa6 Mon Sep 17 00:00:00 2001 From: RFBomb <20431767+RFBomb@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:26:53 -0400 Subject: [PATCH 03/16] Fix Tests not being async --- .../FactoryCommandTests.cs | 68 ++++++++--------- .../IFileCopierTests.cs | 2 +- .../RoboMoverTests.cs | 73 ++++++++++--------- RoboSharp.Extensions.UnitTests/TestPrep.cs | 20 +++-- .../Windows/CopyFileExTests.cs | 5 ++ RoboSharpUnitTesting/JobOptionsTests.cs | 6 +- RoboSharpUnitTesting/LoggingOptionsTests.cs | 22 +++--- .../ProgressEstimatorTests.cs | 73 ++++++++++--------- RoboSharpUnitTesting/RoboQueueEventTests.cs | 46 ++++++------ 9 files changed, 164 insertions(+), 151 deletions(-) diff --git a/RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs b/RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs index ebeb77a4..eda77a98 100644 --- a/RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs +++ b/RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs @@ -74,12 +74,12 @@ public void IsAllowedDir(bool expected, string path) /// Copy Test will use a standard ROBOCOPY command /// [TestMethod] - [Timeout(10000)] + [Timeout(30000)] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Mirror }, DisplayName = "Mirror")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Default }, DisplayName = "Defaults")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectories }, DisplayName = "Subdirectories")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectoriesIncludingEmpty }, DisplayName = "EmptySubdirectories")] - public void CopyTest(object[] flags) + public async Task CopyTest(object[] flags) { Test_Setup.ClearOutTestDestination(); CopyActionFlags copyAction = (CopyActionFlags)flags[2]; @@ -90,11 +90,11 @@ public void CopyTest(object[] flags) var crc = GetCommand(rc); rc.LoggingOptions.ListOnly = true; - var results1 = TestPrep.RunTests(rc, crc, false).Result; + var results1 = await TestPrep.RunTests(rc, crc, false); TestPrep.CompareTestResults(results1[0], results1[1], rc.LoggingOptions.ListOnly); rc.LoggingOptions.ListOnly = false; - var results2 = TestPrep.RunTests(rc, crc, true).Result; + var results2 = await TestPrep.RunTests(rc, crc, true); TestPrep.CompareTestResults(results2[0], results2[1], rc.LoggingOptions.ListOnly); } @@ -105,12 +105,12 @@ private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags se rm = GetCommand(rc, Mocks.MockFileCopierFactory.Instance); } - private static void PrepMoveFiles() + private static async Task PrepMoveFiles() { var rc = TestPrep.GetRoboCommand(false, CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction); rc.CopyOptions.Destination = GetMoveSource(); Directory.CreateDirectory(rc.CopyOptions.Destination); - rc.Start().Wait(); + await rc.Start(); var results = rc.GetResults(); if (results.RoboCopyErrors.Length > 0) throw new Exception( @@ -136,12 +136,12 @@ private static void PrepMoveFiles() [DataRow(data: new object[] { Move | CopyActionFlags.CopySubdirectories, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories | Move Files and Directories")] [DataRow(data: new object[] { Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories-Empty | Move Files")] [DataRow(data: new object[] { Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories-Empty | Move Files and Directories")] - public void MoveTest(object[] flags) + public async Task MoveTest(object[] flags) { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } @@ -151,13 +151,13 @@ public void MoveTest(object[] flags) [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] - public void FileInclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + public async Task FileInclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; rc.CopyOptions.FileFilter = new string[] { "*.txt" }; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } @@ -167,14 +167,14 @@ public void FileInclusionTest(object[] flags) //CopyActionFlags copyAction, Sele [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] - public void FileExclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + public async Task FileExclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); rc.SelectionOptions.ExcludedFiles.Add("*.txt"); rc.Configuration.EnableFileLogging = true; bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } @@ -186,17 +186,17 @@ public void FileExclusionTest(object[] flags) //CopyActionFlags copyAction, Sele [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] - public void ExtraFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + public async Task ExtraFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2] | LoggingFlags.ReportExtraFiles, out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, CreateFile).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); - void CreateFile() + static async Task CreateFile() { - PrepMoveFiles(); + await PrepMoveFiles(); string path = Path.Combine(TestPrep.DestDirPath, "ExtraFileTest.txt"); if (!File.Exists(path)) { @@ -212,17 +212,17 @@ void CreateFile() [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] - public void SameFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + public async Task SameFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, CreateFile).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); - void CreateFile() + static async Task CreateFile() { - PrepMoveFiles(); + await PrepMoveFiles(); Directory.CreateDirectory(TestPrep.DestDirPath); string fn = "1024_Bytes.txt"; string dest = Path.Combine(TestPrep.DestDirPath, fn); @@ -260,13 +260,13 @@ void CreateFile() [DataRow(2, true, Mov_, LoggingFlags.ReportExtraFiles)] [DataRow(2, true, Mov_ | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] [DataRow(2, true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] - public void Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.Depth = depth; - RunPurge(cmd, mover); + await RunPurge(cmd, mover); } [TestMethod] @@ -277,12 +277,12 @@ public void Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, Logging [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] [DataRow(false, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(false, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] - public void Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) + public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedFiles.Add("*0*_Bytes.txt"); - RunPurge(cmd, mover); + await RunPurge(cmd, mover); } [TestMethod] @@ -293,14 +293,14 @@ public void Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] - public void Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) + public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder1"); // Top level empty cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder4"); // Bottom level empty cmd.SelectionOptions.ExcludedDirectories.Add("SubFolder_2a"); // folder with contents - RunPurge(cmd, mover); + await RunPurge(cmd, mover); } [TestMethod] @@ -317,36 +317,36 @@ public void Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) [DataRow(true, Move | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] - public void Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + public async Task Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.FileFilter = new string[] { "*0*_Bytes.txt" }; - RunPurge(cmd, mover); + await RunPurge(cmd, mover); } - private void RunPurge(RoboCommand cmd, FactoryCommand mover) + private async Task RunPurge(RoboCommand cmd, FactoryCommand mover) { //if (Test_Setup.IsRunningOnAppVeyor()) return; - var results = TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, CreateFilesToPurge).Result; + var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, CreateFilesToPurge); TestPrep.CompareTestResults(results[0], results[1], cmd.LoggingOptions.ListOnly); } [TestMethod] [Timeout(10000)] - public void CreateFilesToPurge() + public async Task CreateFilesToPurge() { - PrepMoveFiles(); + await PrepMoveFiles(); RoboCommand prep = new RoboCommand(); prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_1"); prep.CopyOptions.Destination = Path.Combine(Test_Setup.TestDestination, "SubFolder_3"); prep.CopyOptions.ApplyActionFlags(CopyActionFlags.CopySubdirectoriesIncludingEmpty); Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder1", "EmptyFolder2")); - prep.Start().Wait(); + await prep.Start(); prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_2"); prep.CopyOptions.Destination = Path.Combine(prep.CopyOptions.Destination, "SubFolder_2a"); - prep.Start().Wait(); + await prep.Start(); Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder3", "EmptyFolder4")); } } diff --git a/RoboSharp.Extensions.UnitTests/IFileCopierTests.cs b/RoboSharp.Extensions.UnitTests/IFileCopierTests.cs index 9daa011a..d5b433ba 100644 --- a/RoboSharp.Extensions.UnitTests/IFileCopierTests.cs +++ b/RoboSharp.Extensions.UnitTests/IFileCopierTests.cs @@ -319,7 +319,7 @@ public async Task AttributesCopiedProperlyTest(IFileCopier copier) { string fileCopyToDest = copier.Destination.FullName + "_control"; - string? sourceMD5 = null; string? destinationMD5 = null; string controlMD5 = null; + string sourceMD5 = null; string destinationMD5 = null; string controlMD5 = null; try { diff --git a/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs b/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs index 864701b1..72d6a1dc 100644 --- a/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs +++ b/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; namespace RoboSharp.Extensions.Tests { @@ -47,7 +48,7 @@ public void IsAllowedDir(bool expected, string path) [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Default }, DisplayName = "Defaults")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectories }, DisplayName = "Subdirectories")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectoriesIncludingEmpty }, DisplayName = "EmptySubdirectories")] - public void CopyTest(object[] flags) + public async Task CopyTest(object[] flags) { Test_Setup.ClearOutTestDestination(); CopyActionFlags copyAction = (CopyActionFlags)flags[2]; @@ -58,11 +59,11 @@ public void CopyTest(object[] flags) var crc = TestPrep.GetIRoboCommand(rc); rc.LoggingOptions.ListOnly = true; - var results1 = TestPrep.RunTests(rc, crc, false).Result; + var results1 = await TestPrep.RunTests(rc, crc, false); TestPrep.CompareTestResults(results1[0], results1[1], rc.LoggingOptions.ListOnly); rc.LoggingOptions.ListOnly = false; - var results2 = TestPrep.RunTests(rc, crc, true).Result; + var results2 = await TestPrep.RunTests(rc, crc, true); TestPrep.CompareTestResults(results2[0], results2[1], rc.LoggingOptions.ListOnly); } @@ -73,12 +74,12 @@ private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags se rm = TestPrep.GetIRoboCommand(rc); } - private static void PrepMoveFiles() + private static async Task PrepMoveFiles() { var rc = TestPrep.GetRoboCommand(false, CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction); rc.CopyOptions.Destination = GetMoveSource(); Directory.CreateDirectory(rc.CopyOptions.Destination); - rc.Start().Wait(); + await rc.Start(); var results = rc.GetResults(); if (results.RoboCopyErrors.Length > 0) throw new Exception( @@ -103,12 +104,12 @@ private static void PrepMoveFiles() [DataRow(data: new object[] { Move | CopyActionFlags.CopySubdirectories, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories | Move Files and Directories")] [DataRow(data: new object[] { Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories-Empty | Move Files")] [DataRow(data: new object[] { Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Subdirectories-Empty | Move Files and Directories")] - public void MoveTest(object[] flags) + public async Task MoveTest(object[] flags) { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } @@ -117,13 +118,13 @@ public void MoveTest(object[] flags) [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] - public void FileInclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + public async Task FileInclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; rc.CopyOptions.FileFilter = new string[] { "*.txt" }; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } @@ -132,14 +133,14 @@ public void FileInclusionTest(object[] flags) //CopyActionFlags copyAction, Sele [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] - public void FileExclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + public async Task FileExclusionTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); rc.SelectionOptions.ExcludedFiles.Add("*.txt"); rc.Configuration.EnableFileLogging = true; bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } @@ -150,17 +151,17 @@ public void FileExclusionTest(object[] flags) //CopyActionFlags copyAction, Sele [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] - public void ExtraFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + public async Task ExtraFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2] | LoggingFlags.ReportExtraFiles, out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, CreateFile).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); - void CreateFile() + static async Task CreateFile() { - PrepMoveFiles(); + await PrepMoveFiles(); string path = Path.Combine(TestPrep.DestDirPath, "ExtraFileTest.txt"); if (!File.Exists(path)) { @@ -175,17 +176,17 @@ void CreateFile() [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files and Directories")] - public void SameFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction + public async Task SameFileTest(object[] flags) //CopyActionFlags copyAction, SelectionFlags selectionFlags, LoggingFlags loggingAction { if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = TestPrep.RunTests(rc, rm, !listOnly, CreateFile).Result; + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); - void CreateFile() + static async Task CreateFile() { - PrepMoveFiles(); + await PrepMoveFiles(); Directory.CreateDirectory(TestPrep.DestDirPath); string fn = "1024_Bytes.txt"; string dest = Path.Combine(TestPrep.DestDirPath, fn); @@ -222,13 +223,13 @@ void CreateFile() [DataRow(2, true, Mov_ | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] [DataRow(2, true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] [TestMethod] - public void Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.Depth = depth; - RunPurge(cmd, mover); + await RunPurge(cmd, mover); } [DataRow(true, Mov_)] @@ -238,12 +239,12 @@ public void Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, Logging [DataRow(false, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(false, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [TestMethod] - public void Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) + public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedFiles.Add("*0*_Bytes.txt"); - RunPurge(cmd, mover); + await RunPurge(cmd, mover); } [DataRow(true, Mov_)] @@ -253,14 +254,14 @@ public void Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [TestMethod] - public void Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) + public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder1"); // Top level empty cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder4"); // Bottom level empty cmd.SelectionOptions.ExcludedDirectories.Add("SubFolder_2a"); // folder with contents - RunPurge(cmd, mover); + await RunPurge(cmd, mover); } [DataRow(true, Mov_)] @@ -276,19 +277,19 @@ public void Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] [TestMethod] - public void Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + public async Task Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.FileFilter = new string[] { "*0*_Bytes.txt" }; - RunPurge(cmd, mover); + await RunPurge(cmd, mover); } - private void RunPurge(RoboCommand cmd, RoboMover mover) + private async Task RunPurge(RoboCommand cmd, RoboMover mover) { //if (Test_Setup.IsRunningOnAppVeyor()) return; - var results = TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, CreateFilesToPurge).Result; + var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, CreateFilesToPurge); TestPrep.CompareTestResults(results[0], results[1], cmd.LoggingOptions.ListOnly); } @@ -297,7 +298,7 @@ private void RunPurge(RoboCommand cmd, RoboMover mover) [DataRow(CopyActionFlags.MoveFilesAndDirectories)] [DataRow(CopyActionFlags.MoveFilesAndDirectories | CopyActionFlags.Purge)] [DataTestMethod] - public void ValidateRoboMover(CopyActionFlags copyOptions) + public async Task ValidateRoboMover(CopyActionFlags copyOptions) { GetMoveCommands( CopyActionFlags.CopySubdirectoriesIncludingEmpty | copyOptions, @@ -305,7 +306,7 @@ public void ValidateRoboMover(CopyActionFlags copyOptions) DefaultLoggingAction, out _, out var rm); Test_Setup.ClearOutTestDestination(); - PrepMoveFiles(); + await PrepMoveFiles(); string subfolderpath = @"SubFolder_1\SubFolder_1.1\SubFolder_1.2"; FilePair[] SourceFiles = new FilePair[] { @@ -331,7 +332,7 @@ public void ValidateRoboMover(CopyActionFlags copyOptions) foreach (var dir in PurgeDirectories) Directory.CreateDirectory(dir.FullName); foreach (var file in purgeFiles) File.WriteAllText(file.FullName, "PURGE ME"); - rm.Start().Wait(); + await rm.Start(); foreach (var lin in rm.GetResults().LogLines) Console.WriteLine(lin); @@ -360,18 +361,18 @@ public void ValidateRoboMover(CopyActionFlags copyOptions) } [DataTestMethod] - public void CreateFilesToPurge() + public async Task CreateFilesToPurge() { - PrepMoveFiles(); + await PrepMoveFiles(); RoboCommand prep = new RoboCommand(); prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_1"); prep.CopyOptions.Destination = Path.Combine(Test_Setup.TestDestination, "SubFolder_3"); prep.CopyOptions.ApplyActionFlags(CopyActionFlags.CopySubdirectoriesIncludingEmpty); Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder1", "EmptyFolder2")); - prep.Start().Wait(); + await prep.Start(); prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_2"); prep.CopyOptions.Destination = Path.Combine(prep.CopyOptions.Destination, "SubFolder_2a"); - prep.Start().Wait(); + await prep.Start(); Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder3", "EmptyFolder4")); } } diff --git a/RoboSharp.Extensions.UnitTests/TestPrep.cs b/RoboSharp.Extensions.UnitTests/TestPrep.cs index 14cb5608..18cf41e3 100644 --- a/RoboSharp.Extensions.UnitTests/TestPrep.cs +++ b/RoboSharp.Extensions.UnitTests/TestPrep.cs @@ -54,12 +54,19 @@ public static RoboCommand GetRoboCommand(bool useLargerFileSet, CopyActionFlags return cmd; } - public static async Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns, Action actionBetweenRuns = null) + + public static Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns) + => RunTests(roboCommand, customCommand, CleanBetweenRuns, taskBetweenRuns: null); + + public static Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns, Action actionBetweenRuns) + => RunTests(roboCommand, customCommand, CleanBetweenRuns, taskBetweenRuns: actionBetweenRuns is null ? null : () => Task.Run(actionBetweenRuns)); + + public static async Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns, Func taskBetweenRuns) { var results = new List(); - BetweenRuns(); + await BetweenRuns(); results.Add(await TestSetup.RunTest(roboCommand)); - if (!roboCommand.LoggingOptions.ListOnly) BetweenRuns(); + if (!roboCommand.LoggingOptions.ListOnly) await BetweenRuns(); customCommand.OnError += CachedRoboCommand_OnError; customCommand.OnCommandError += CachedRoboCommand_OnCommandError; @@ -72,13 +79,12 @@ public static async Task RunTests(RoboCommand roboComman if (CleanBetweenRuns) TestSetup.ClearOutTestDestination(); return results.ToArray(); - void BetweenRuns() + async Task BetweenRuns() { if (CleanBetweenRuns) TestSetup.ClearOutTestDestination(); - actionBetweenRuns?.Invoke(); + if (taskBetweenRuns is not null) + await taskBetweenRuns(); } - - } private static void CachedRoboCommand_OnCommandError(IRoboCommand sender, CommandErrorEventArgs e) => Console.WriteLine(e.Exception); private static void CachedRoboCommand_OnError(IRoboCommand sender, RoboSharp.ErrorEventArgs e) => Console.WriteLine(e.Error); diff --git a/RoboSharp.Extensions.UnitTests/Windows/CopyFileExTests.cs b/RoboSharp.Extensions.UnitTests/Windows/CopyFileExTests.cs index 42f12579..c8ab0eb1 100644 --- a/RoboSharp.Extensions.UnitTests/Windows/CopyFileExTests.cs +++ b/RoboSharp.Extensions.UnitTests/Windows/CopyFileExTests.cs @@ -8,6 +8,8 @@ using System.Threading.Tasks; using static RoboSharp.Extensions.Tests.AssertExtensions; +#pragma warning disable CA1416 // Validate platform compatibility + namespace RoboSharp.Extensions.Windows.UnitTests { [TestClass()] @@ -40,6 +42,7 @@ public void TestCancellationTokens() [TestMethod()] public async Task IFileCopierFactoryTests_CopyFileEx() { + IFileCopierFactory factory = new CopyFileExFactory() { Options = CopyFileExOptions.NONE }; if (VersionManager.IsPlatformWindows) { @@ -356,3 +359,5 @@ void Cancel(object o, T obj) } } } + +#pragma warning restore CA1416 // Validate platform compatibility \ No newline at end of file diff --git a/RoboSharpUnitTesting/JobOptionsTests.cs b/RoboSharpUnitTesting/JobOptionsTests.cs index 9d369d74..61a345c0 100644 --- a/RoboSharpUnitTesting/JobOptionsTests.cs +++ b/RoboSharpUnitTesting/JobOptionsTests.cs @@ -59,7 +59,7 @@ public void RoboCopyJobFileOutput(string fileName, string arguments) /// This test ensures that the destination directory is not created when using the /QUIT function ///
[TestMethod] - public void TestPreventCopy() + public async Task TestPreventCopy() { RoboCommand cmd = new RoboCommand(source: Test_Setup.Source_Standard, destination: Path.Combine(Test_Setup.TestDestination, Path.GetRandomFileName())); Console.WriteLine("Destination Path: " + cmd.CopyOptions.Destination); @@ -71,10 +71,10 @@ public void TestPreventCopy() Authentication.AuthenticateDestination(cmd); Assert.IsFalse(Directory.Exists(cmd.CopyOptions.Destination), "\nDestination Directory was created during authentication!"); - cmd.Start().Wait(); + await cmd.Start(); Assert.IsFalse(Directory.Exists(cmd.CopyOptions.Destination), "\nDestination Directory was created when running the command!"); cmd.JobOptions.PreventCopyOperation = false; - cmd.Start().Wait(); + await cmd.Start(); Assert.IsTrue(Directory.Exists(cmd.CopyOptions.Destination), "\nDestination Directory was not created."); } finally diff --git a/RoboSharpUnitTesting/LoggingOptionsTests.cs b/RoboSharpUnitTesting/LoggingOptionsTests.cs index 286029a5..c84f8021 100644 --- a/RoboSharpUnitTesting/LoggingOptionsTests.cs +++ b/RoboSharpUnitTesting/LoggingOptionsTests.cs @@ -14,7 +14,7 @@ public class LoggingOptionsTests /// This test ensures that the destination directory is not created when using the /QUIT function /// [TestMethod] - public void TestListOnlyDestinationCreation() + public async Task TestListOnlyDestinationCreation() { RoboCommand cmd = new RoboCommand(source: Test_Setup.Source_Standard, destination: Path.Combine(Test_Setup.TestDestination, Path.GetRandomFileName())); Console.WriteLine("Destination Path: " + cmd.CopyOptions.Destination); @@ -26,19 +26,19 @@ public void TestListOnlyDestinationCreation() Assert.IsFalse(Directory.Exists(cmd.CopyOptions.Destination), "\nDestination Directory was created during authentication!"); cmd.LoggingOptions.ListOnly = false; - cmd.Start_ListOnly().Wait(); + await cmd.Start_ListOnly(); Assert.IsFalse(Directory.Exists(cmd.CopyOptions.Destination), "\nStart_ListOnly() - Destination Directory was created!"); cmd.LoggingOptions.ListOnly = false; - cmd.StartAsync_ListOnly().Wait(); + await cmd.StartAsync_ListOnly(); Assert.IsFalse(Directory.Exists(cmd.CopyOptions.Destination), "\nStartAsync_ListOnly() - Destination Directory was created!"); cmd.LoggingOptions.ListOnly = true; - cmd.Start().Wait(); + await cmd.Start(); Assert.IsFalse(Directory.Exists(cmd.CopyOptions.Destination), "\nList-Only Setting - Destination Directory was created!"); cmd.LoggingOptions.ListOnly = false; - cmd.Start().Wait(); + await cmd.Start(); Assert.IsTrue(Directory.Exists(cmd.CopyOptions.Destination), "\nDestination Directory was not created."); } @@ -74,11 +74,11 @@ public void TestIsLogFileSpecified() [DataRow(true)] [DataRow(false)] [TestMethod] - public void TestBytes(bool withBytes) + public async Task TestBytes(bool withBytes) { RoboCommand cmd = Test_Setup.GenerateCommand(false, true); cmd.LoggingOptions.PrintSizesAsBytes = withBytes; - cmd.Start().Wait(); + await cmd.Start(); var results = cmd.GetResults(); Assert.IsNotNull(results); results.LogLines.ToList().ForEach(Console.WriteLine); @@ -90,12 +90,12 @@ public void TestBytes(bool withBytes) [DataRow(true, false)] [DataRow(false, false)] [TestMethod] - public void ConfigurationLoggingEnabled(bool isEnabled, bool listOnly) + public async Task ConfigurationLoggingEnabled(bool isEnabled, bool listOnly) { Test_Setup.ClearOutTestDestination(); RoboCommand cmd = Test_Setup.GenerateCommand(false, listOnly); cmd.Configuration.EnableFileLogging = isEnabled; - cmd.Start().Wait(); + await cmd.Start(); var results = cmd.GetResults(); Assert.IsNotNull(results); results.LogLines.ToList().ForEach(Console.WriteLine); @@ -106,14 +106,14 @@ public void ConfigurationLoggingEnabled(bool isEnabled, bool listOnly) [DataRow(true, false, DisplayName = "No Summary")] [DataRow(false, false, DisplayName = "No Header, No Summary")] [TestMethod] - public void TestSummaryAndHeader(bool header, bool summary) + public async Task TestSummaryAndHeader(bool header, bool summary) { Test_Setup.ClearOutTestDestination(); RoboCommand cmd = Test_Setup.GenerateCommand(false, true); //cmd.Configuration.EnableFileLogging = true; cmd.LoggingOptions.NoJobHeader = !header; cmd.LoggingOptions.NoJobSummary= !summary; - cmd.Start().Wait(); + await cmd.Start(); var results = cmd.GetResults(); Assert.IsNotNull(results); results.LogLines.ToList().ForEach(Console.WriteLine); diff --git a/RoboSharpUnitTesting/ProgressEstimatorTests.cs b/RoboSharpUnitTesting/ProgressEstimatorTests.cs index 0e0365f9..b87e0255 100644 --- a/RoboSharpUnitTesting/ProgressEstimatorTests.cs +++ b/RoboSharpUnitTesting/ProgressEstimatorTests.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; namespace RoboSharp.UnitTests { @@ -25,67 +26,67 @@ public class ProgressEstimatorTests public virtual bool ListOnlyMode => false; //[TestMethod] - public void SAMPLE_TEST_METHOD() + public async Task SAMPLE_TEST_METHOD() { // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(false, ListOnlyMode); //Run the test and Evaluate the results and pass/Fail the test - RoboSharpTestResults UnitTestResults = Test_Setup.RunTest(cmd).Result; + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); UnitTestResults.AssertTest(); } [TestMethod] - public void Test_NoCopyOptions() + public async Task Test_NoCopyOptions() { // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(false, ListOnlyMode); //Run the test - First Test should just use default values generated from the GenerateCommand method! Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = Test_Setup.RunTest(cmd).Result; + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); //Evaluate the results and pass/Fail the test UnitTestResults.AssertTest(); } [TestMethod] - public void Test_ExcludedFiles() + public async Task Test_ExcludedFiles() { // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(false, ListOnlyMode); cmd.SelectionOptions.ExcludedFiles.Add("4_Bytes.txt"); // 3 copies of this file exist Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = Test_Setup.RunTest(cmd).Result; + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); UnitTestResults.AssertTest();//Evaluate the results and pass/Fail the test } [TestMethod] - public void Test_MinFileSize() + public async Task Test_MinFileSize() { // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(true, ListOnlyMode); cmd.SelectionOptions.MinFileSize = 1500; Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = Test_Setup.RunTest(cmd).Result; + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); UnitTestResults.AssertTest();//Evaluate the results and pass/Fail the test } [TestMethod] - public void Test_MaxFileSize() + public async Task Test_MaxFileSize() { // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(true, ListOnlyMode); cmd.SelectionOptions.MaxFileSize = 1500; Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = Test_Setup.RunTest(cmd).Result; + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); UnitTestResults.AssertTest();//Evaluate the results and pass/Fail the test } [TestMethod] - public void Test_FileInUse() + public async Task Test_FileInUse() { if (Test_Setup.IsRunningOnAppVeyor()) return; @@ -111,7 +112,7 @@ public void Test_FileInUse() Console.WriteLine("Creating and locking file: " + fPath); var f = File.Open(fPath, FileMode.Create); Console.WriteLine("Running Test"); - UnitTestResults = Test_Setup.RunTest(cmd).Result; + UnitTestResults = await Test_Setup.RunTest(cmd); Console.WriteLine("Test Complete"); Console.WriteLine("Releasing File: " + fPath); f.Close(); @@ -128,7 +129,7 @@ public void Test_FileInUse() } [TestMethod] - public void Test_ExcludeLastAccessDate() + public async Task Test_ExcludeLastAccessDate() { //Create the command and base values for the Expected Results RoboCommand cmd = Test_Setup.GenerateCommand(false, ListOnlyMode); @@ -143,7 +144,7 @@ public void Test_ExcludeLastAccessDate() cmd.SelectionOptions.MaxLastAccessDate = "19900101"; Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = Test_Setup.RunTest(cmd).Result; + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); //Evaluate the results and pass/Fail the test UnitTestResults.AssertTest(); @@ -158,12 +159,12 @@ public void Test_ExcludeLastAccessDate() [DataRow(1)] [DataRow(8)] [TestMethod] - public void TestMultiThread(int threads) + public async Task TestMultiThread(int threads) { RoboCommand cmd = Test_Setup.GenerateCommand(false, ListOnlyMode); cmd.CopyOptions.MultiThreadedCopiesCount = threads; Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = Test_Setup.RunTest(cmd).Result; + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); // Ignore Directory Statistics during a multithread test, as they are not reported by robocopy List Errors = new List(); @@ -195,33 +196,33 @@ public void TestMultiThread(int threads) */ //INCLUDE - [TestMethod] public void Test_IncludeAttribReadOnly() => Test_Attributes(FileAttributes.ReadOnly, true); - [TestMethod] public void Test_IncludeAttribArchive() => Test_Attributes(FileAttributes.Archive, true); - [TestMethod] public void Test_IncludeAttribSystem() => Test_Attributes(FileAttributes.System, true); - [TestMethod] public void Test_IncludeAttribHidden() => Test_Attributes(FileAttributes.Hidden, true); - //[TestMethod] public void Test_IncludeAttribCompressed() => Test_Attributes(FileAttributes.Compressed, true); - [TestMethod] public void Test_IncludeAttribNotContentIndexed() => Test_Attributes(FileAttributes.NotContentIndexed, true); - //[TestMethod] public void Test_IncludeAttribEncrypted() => Test_Attributes(FileAttributes.Encrypted, true); - [TestMethod] public void Test_IncludeAttribTemporary() => Test_Attributes(FileAttributes.Temporary, true); - [TestMethod] public void Test_IncludeAttribOffline() => Test_Attributes(FileAttributes.Offline, true); + [TestMethod] public Task Test_IncludeAttribReadOnly() => Test_Attributes(FileAttributes.ReadOnly, true); + [TestMethod] public Task Test_IncludeAttribArchive() => Test_Attributes(FileAttributes.Archive, true); + [TestMethod] public Task Test_IncludeAttribSystem() => Test_Attributes(FileAttributes.System, true); + [TestMethod] public Task Test_IncludeAttribHidden() => Test_Attributes(FileAttributes.Hidden, true); + //[TestMethod] public Task Test_IncludeAttribCompressed() => Test_Attributes(FileAttributes.Compressed, true); + [TestMethod] public Task Test_IncludeAttribNotContentIndexed() => Test_Attributes(FileAttributes.NotContentIndexed, true); + //[TestMethod] public Task Test_IncludeAttribEncrypted() => Test_Attributes(FileAttributes.Encrypted, true); + [TestMethod] public Task Test_IncludeAttribTemporary() => Test_Attributes(FileAttributes.Temporary, true); + [TestMethod] public Task Test_IncludeAttribOffline() => Test_Attributes(FileAttributes.Offline, true); //EXCLUDE - [TestMethod] public void Test_ExcludeAttribReadOnly() => Test_Attributes(FileAttributes.ReadOnly, false); - [TestMethod] public void Test_ExcludeAttribArchive() => Test_Attributes(FileAttributes.Archive, false); - [TestMethod] public void Test_ExcludeAttribSystem() => Test_Attributes(FileAttributes.System, false); - [TestMethod] public void Test_ExcludeAttribHidden() => Test_Attributes(FileAttributes.Hidden, false); - //[TestMethod] public void Test_ExcludeAttribCompressed() => Test_Attributes(FileAttributes.Compressed, false); - [TestMethod] public void Test_ExcludeAttribNotContentIndexed() => Test_Attributes(FileAttributes.NotContentIndexed, false); - //[TestMethod] public void Test_ExcludeAttribEncrypted() => Test_Attributes(FileAttributes.Encrypted, false); - [TestMethod] public void Test_ExcludeAttribTemporary() => Test_Attributes(FileAttributes.Temporary, false); - [TestMethod] public void Test_ExcludeAttribOffline() => Test_Attributes(FileAttributes.Offline, false); + [TestMethod] public Task Test_ExcludeAttribReadOnly() => Test_Attributes(FileAttributes.ReadOnly, false); + [TestMethod] public Task Test_ExcludeAttribArchive() => Test_Attributes(FileAttributes.Archive, false); + [TestMethod] public Task Test_ExcludeAttribSystem() => Test_Attributes(FileAttributes.System, false); + [TestMethod] public Task Test_ExcludeAttribHidden() => Test_Attributes(FileAttributes.Hidden, false); + //[TestMethod] public Task Test_ExcludeAttribCompressed() => Test_Attributes(FileAttributes.Compressed, false); + [TestMethod] public Task Test_ExcludeAttribNotContentIndexed() => Test_Attributes(FileAttributes.NotContentIndexed, false); + //[TestMethod] public Task Test_ExcludeAttribEncrypted() => Test_Attributes(FileAttributes.Encrypted, false); + [TestMethod] public Task Test_ExcludeAttribTemporary() => Test_Attributes(FileAttributes.Temporary, false); + [TestMethod] public Task Test_ExcludeAttribOffline() => Test_Attributes(FileAttributes.Offline, false); #pragma warning disable IDE0059 // Unnecessary assignment of a value /// /// TRUE if setting to INCLUDE, False to EXCLUDE - private void Test_Attributes(FileAttributes attributes, bool Include) + private async Task Test_Attributes(FileAttributes attributes, bool Include) { // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(false, ListOnlyMode); @@ -261,7 +262,7 @@ private void Test_Attributes(FileAttributes attributes, bool Include) } Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = Test_Setup.RunTest(cmd).Result; + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); //Revert all modified files to their normal state File.SetAttributes(filePath, FileAttributes.Normal); //Source File diff --git a/RoboSharpUnitTesting/RoboQueueEventTests.cs b/RoboSharpUnitTesting/RoboQueueEventTests.cs index 212e1774..1f42026d 100644 --- a/RoboSharpUnitTesting/RoboQueueEventTests.cs +++ b/RoboSharpUnitTesting/RoboQueueEventTests.cs @@ -17,44 +17,44 @@ private static RoboQueue GenerateRQ(out RoboCommand cmd) return new RoboQueue(cmd); } - private static void RunTestThenAssert(RoboQueue Q, ref bool testPassed) + private static async Task RunTestThenAssert(RoboQueue Q, Func testPassed) { - Q.StartAll().Wait(); + await Q.StartAll(); if (Q.RunResults.Count > 0) Test_Setup.WriteLogLines(Q.RunResults[0], true); - if (!testPassed) throw new AssertFailedException("Subscribed Event was not Raised!"); + if (!testPassed()) throw new AssertFailedException("Subscribed Event was not Raised!"); } [TestMethod] - public void RoboQueue_OnCommandCompleted() + public async Task RoboQueue_OnCommandCompleted() { var RQ = GenerateRQ(out RoboCommand cmd); bool TestPassed = false; RQ.OnCommandCompleted += (o, e) => TestPassed = true; - RunTestThenAssert(RQ, ref TestPassed); + await RunTestThenAssert(RQ, () => TestPassed); } [TestMethod] - public void RoboQueue_OnCommandError() + public async Task RoboQueue_OnCommandError() { var RQ = GenerateRQ(out RoboCommand cmd); cmd.CopyOptions.Source += "FolderDoesNotExist"; bool TestPassed = false; RQ.OnCommandError += (o, e) => TestPassed = true; - RunTestThenAssert(RQ, ref TestPassed); + await RunTestThenAssert(RQ, () => TestPassed); } [TestMethod] - public void RoboQueue_OnCopyProgressChanged() + public async Task RoboQueue_OnCopyProgressChanged() { var RQ = GenerateRQ(out RoboCommand cmd); Test_Setup.ClearOutTestDestination(); bool TestPassed = false; RQ.OnCopyProgressChanged += (o, e) => TestPassed = true; - RunTestThenAssert(RQ, ref TestPassed); + await RunTestThenAssert(RQ, () => TestPassed); } [TestMethod] - public void RoboQueue_OnError() + public async Task RoboQueue_OnError() { if (Test_Setup.IsRunningOnAppVeyor()) return; @@ -68,64 +68,64 @@ public void RoboQueue_OnError() { f.WriteLine("StartTest!"); Console.WriteLine("Expecting 1 File Failed!\n\n"); - RunTestThenAssert(RQ, ref TestPassed); + await RunTestThenAssert(RQ, () => TestPassed); } } [TestMethod] - public void RoboQueue_OnFileProcessed() + public async Task RoboQueue_OnFileProcessed() { var RQ = GenerateRQ(out RoboCommand cmd); Test_Setup.ClearOutTestDestination(); bool TestPassed = false; RQ.OnFileProcessed += (o, e) => TestPassed = true; - RunTestThenAssert(RQ, ref TestPassed); + await RunTestThenAssert(RQ, () => TestPassed); } [TestMethod] - public void RoboQueue_ProgressEstimatorCreated() + public async Task RoboQueue_ProgressEstimatorCreated() { var RQ = GenerateRQ(out RoboCommand cmd); bool TestPassed = false; RQ.OnProgressEstimatorCreated += (o, e) => TestPassed = true; - RunTestThenAssert(RQ, ref TestPassed); + await RunTestThenAssert(RQ, () => TestPassed); } [TestMethod] - public void RoboQueue_OnCommandStarted() + public async Task RoboQueue_OnCommandStarted() { var RQ = GenerateRQ(out RoboCommand cmd); bool TestPassed = false; RQ.OnCommandStarted += (o, e) => TestPassed = true; - RunTestThenAssert(RQ, ref TestPassed); + await RunTestThenAssert(RQ, () => TestPassed); } [TestMethod] - public void RoboQueue_RunCompleted() + public async Task RoboQueue_RunCompleted() { var RQ = GenerateRQ(out RoboCommand cmd); bool TestPassed = false; RQ.RunCompleted += (o, e) => TestPassed = true; - RunTestThenAssert(RQ, ref TestPassed); + await RunTestThenAssert(RQ, () => TestPassed); } [TestMethod] - public void RoboQueue_RunResultsUpdated() + public async Task RoboQueue_RunResultsUpdated() { var RQ = GenerateRQ(out RoboCommand cmd); bool TestPassed = false; RQ.RunResultsUpdated += (o, e) => TestPassed = true; - RunTestThenAssert(RQ, ref TestPassed); + await RunTestThenAssert(RQ, () => TestPassed); } [TestMethod] - public void RoboQueue_ListResultsUpdated() + public async Task RoboQueue_ListResultsUpdated() { var RQ = GenerateRQ(out RoboCommand cmd); bool TestPassed = false; RQ.ListResultsUpdated += (o, e) => TestPassed = true; - RQ.StartAll_ListOnly().Wait(); + await RQ.StartAll_ListOnly(); if (!TestPassed) throw new AssertFailedException("ListResultsUpdated Event was not Raised!"); } From 3c1ed1eae21509aaf0fc1c1c6f702d5277506769 Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:17:14 -0400 Subject: [PATCH 04/16] Rename FactoryCommand to RoboCommandPortable - Since this new IRoboCommand is designed to be portable between platforms, the class name has been updated to reflect that. --- ...andTests.cs => RoboCommandPortableTests.cs} | 16 ++++++++-------- ...actoryCommand.cs => RoboCommandPortable.cs} | 8 ++++---- ...actory.cs => RoboCommandPortableFactory.cs} | 18 +++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) rename RoboSharp.Extensions.UnitTests/{FactoryCommandTests.cs => RoboCommandPortableTests.cs} (96%) rename RoboSharp.Extensions/{FactoryCommand.cs => RoboCommandPortable.cs} (98%) rename RoboSharp.Extensions/{FactoryCommandFactory.cs => RoboCommandPortableFactory.cs} (73%) diff --git a/RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs b/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs similarity index 96% rename from RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs rename to RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs index eda77a98..04f470ad 100644 --- a/RoboSharp.Extensions.UnitTests/FactoryCommandTests.cs +++ b/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs @@ -15,15 +15,15 @@ namespace RoboSharp.Extensions.Tests { /// - /// Test the object + /// Test the object /// [TestClass] - public class FactoryCommand_EventTests : RoboSharp.UnitTests.RoboCommandEventTests + public class RoboCommandPortable_EventTests : RoboSharp.UnitTests.RoboCommandEventTests { protected override IRoboCommand GenerateCommand(bool UseLargerFileSet, bool ListOnlyMode) { var rc = RoboSharp.UnitTests.Test_Setup.GenerateCommand(false, true); - var command = new FactoryCommand(RoboSharp.Extensions.StreamedCopierFactory.DefaultFactory) + var command = new RoboCommandPortable(RoboSharp.Extensions.StreamedCopierFactory.DefaultFactory) { CopyOptions = rc.CopyOptions, SelectionOptions = rc.SelectionOptions, @@ -39,13 +39,13 @@ protected override IRoboCommand GenerateCommand(bool UseLargerFileSet, bool List /// Validate that the command works the same as robocopy /// [TestClass] - public class FactoryCommand_Tests + public class RoboCommandPortable_Tests { const LoggingFlags DefaultLoggingAction = LoggingFlags.RoboSharpDefault | LoggingFlags.NoJobHeader; - private static FactoryCommand GetCommand(RoboCommand rc, IFileCopierFactory factory = null) + private static RoboCommandPortable GetCommand(RoboCommand rc, IFileCopierFactory factory = null) { - return new FactoryCommand(factory ?? RoboSharp.Extensions.StreamedCopierFactory.DefaultFactory) + return new RoboCommandPortable(factory ?? RoboSharp.Extensions.StreamedCopierFactory.DefaultFactory) { CopyOptions = rc.CopyOptions, SelectionOptions = rc.SelectionOptions, @@ -98,7 +98,7 @@ public async Task CopyTest(object[] flags) TestPrep.CompareTestResults(results2[0], results2[1], rc.LoggingOptions.ListOnly); } - private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags selectionFlags, LoggingFlags loggingFlags, out RoboCommand rc, out FactoryCommand rm) + private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags selectionFlags, LoggingFlags loggingFlags, out RoboCommand rc, out RoboCommandPortable rm) { rc = TestPrep.GetRoboCommand(false, copyFlags, selectionFlags, loggingFlags); rc.CopyOptions.Source = GetMoveSource(); @@ -326,7 +326,7 @@ public async Task Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, Logg await RunPurge(cmd, mover); } - private async Task RunPurge(RoboCommand cmd, FactoryCommand mover) + private async Task RunPurge(RoboCommand cmd, RoboCommandPortable mover) { //if (Test_Setup.IsRunningOnAppVeyor()) return; var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, CreateFilesToPurge); diff --git a/RoboSharp.Extensions/FactoryCommand.cs b/RoboSharp.Extensions/RoboCommandPortable.cs similarity index 98% rename from RoboSharp.Extensions/FactoryCommand.cs rename to RoboSharp.Extensions/RoboCommandPortable.cs index 3878bc4a..07c499f4 100644 --- a/RoboSharp.Extensions/FactoryCommand.cs +++ b/RoboSharp.Extensions/RoboCommandPortable.cs @@ -26,7 +26,7 @@ namespace RoboSharp.Extensions /// This relies on an to generate the objects used to manage the copy operations. ///
This class should allow use of this library in non-windows environments. /// - public class FactoryCommand : IRoboCommand, INotifyPropertyChanged + public class RoboCommandPortable : IRoboCommand, INotifyPropertyChanged { internal static void ThrowUnsupportedFrameworkException() { @@ -36,7 +36,7 @@ internal static void ThrowUnsupportedFrameworkException() } /// - /// Create a new + /// Create a new /// /// /// @@ -45,7 +45,7 @@ internal static void ThrowUnsupportedFrameworkException() /// /// /// Not Available in .Net Framework or .NetStandard2.0 - public FactoryCommand(IFileCopierFactory fileCopierFactory, IAuthenticator? authenticator = null) + public RoboCommandPortable(IFileCopierFactory fileCopierFactory, IAuthenticator? authenticator = null) { ThrowUnsupportedFrameworkException(); copierFactory = fileCopierFactory ?? throw new ArgumentNullException(nameof(fileCopierFactory)); @@ -242,7 +242,7 @@ private async Task Run(string domain, string username, string password, Action? if (IsRunning) { _startLock.Release(); - throw new InvalidOperationException($"{nameof(FactoryCommand)} is already running."); + throw new InvalidOperationException($"{nameof(RoboCommandPortable)} is already running."); } IsRunning = true; IsPaused = false; diff --git a/RoboSharp.Extensions/FactoryCommandFactory.cs b/RoboSharp.Extensions/RoboCommandPortableFactory.cs similarity index 73% rename from RoboSharp.Extensions/FactoryCommandFactory.cs rename to RoboSharp.Extensions/RoboCommandPortableFactory.cs index 0d5092eb..3050da34 100644 --- a/RoboSharp.Extensions/FactoryCommandFactory.cs +++ b/RoboSharp.Extensions/RoboCommandPortableFactory.cs @@ -6,13 +6,13 @@ namespace RoboSharp.Extensions { /// - /// An that creates objects that will use some to determine how the copy operation is actually performed. + /// An that creates objects that will use some to determine how the copy operation is actually performed. ///
This class should allow use of this library in non-windows environments. ///
- public class FactoryCommandFactory : IRoboCommandFactory + public class RoboCommandPortableFactory : IRoboCommandFactory { /// - /// Create a new to produce objects + /// Create a new to produce objects /// /// The factory to use when a copy or move operation is required /// @@ -21,9 +21,9 @@ public class FactoryCommandFactory : IRoboCommandFactory /// /// /// Applies to .NetFramework and .NetStandard2.0 - public FactoryCommandFactory(IFileCopierFactory fileCopierFactory, IAuthenticator? authenticator = null) + public RoboCommandPortableFactory(IFileCopierFactory fileCopierFactory, IAuthenticator? authenticator = null) { - FactoryCommand.ThrowUnsupportedFrameworkException(); + RoboCommandPortable.ThrowUnsupportedFrameworkException(); _fileCopierFactory = fileCopierFactory ?? throw new ArgumentNullException(nameof(fileCopierFactory)); _authenticator = authenticator ?? SourceAndDestinationAuthenticator.Instance; } @@ -34,13 +34,13 @@ public FactoryCommandFactory(IFileCopierFactory fileCopierFactory, IAuthenticato /// public IRoboCommand GetRoboCommand() { - return new FactoryCommand(_fileCopierFactory, _authenticator); + return new RoboCommandPortable(_fileCopierFactory, _authenticator); } /// public IRoboCommand GetRoboCommand(string source, string destination) { - var cmd = new FactoryCommand(_fileCopierFactory, _authenticator); + var cmd = new RoboCommandPortable(_fileCopierFactory, _authenticator); cmd.CopyOptions.Source = source; cmd.CopyOptions.Destination = destination; return cmd; @@ -49,7 +49,7 @@ public IRoboCommand GetRoboCommand(string source, string destination) /// public IRoboCommand GetRoboCommand(string source, string destination, CopyActionFlags copyActionFlags) { - var cmd = new FactoryCommand(_fileCopierFactory, _authenticator); + var cmd = new RoboCommandPortable(_fileCopierFactory, _authenticator); cmd.CopyOptions.Source = source; cmd.CopyOptions.Destination = destination; cmd.CopyOptions.ApplyActionFlags(copyActionFlags); @@ -59,7 +59,7 @@ public IRoboCommand GetRoboCommand(string source, string destination, CopyAction /// public IRoboCommand GetRoboCommand(string source, string destination, CopyActionFlags copyActionFlags, SelectionFlags selectionFlags) { - var cmd = new FactoryCommand(_fileCopierFactory, _authenticator); + var cmd = new RoboCommandPortable(_fileCopierFactory, _authenticator); cmd.CopyOptions.Source = source; cmd.CopyOptions.Destination = destination; cmd.CopyOptions.ApplyActionFlags(copyActionFlags); From 83208a42a1234e860870f793312b5443cc1a2c04 Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:17:26 -0400 Subject: [PATCH 05/16] Update CopyFileExFactory.cs --- RoboSharp.Extensions/Windows/CopyFileExFactory.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RoboSharp.Extensions/Windows/CopyFileExFactory.cs b/RoboSharp.Extensions/Windows/CopyFileExFactory.cs index b1c7f361..be795225 100644 --- a/RoboSharp.Extensions/Windows/CopyFileExFactory.cs +++ b/RoboSharp.Extensions/Windows/CopyFileExFactory.cs @@ -17,6 +17,9 @@ namespace RoboSharp.Extensions.Windows #endif public sealed class CopyFileExFactory : AbstractFileCopierFactory, IFileCopierFactory { + /// + public CopyFileExFactory() : base() { VersionManager.ThrowIfNotWindowsPlatform(); } + /// /// The options to apply to generated copiers /// From 61de77b7a4777af8cbdd018529657086a32fc67d Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:10:57 -0400 Subject: [PATCH 06/16] Implement RetryOptions --- RoboSharp.Extensions/RoboCommandPortable.cs | 286 ++++++++++-------- .../RoboCommandPortableFactory.cs | 13 +- RoboSharp/RetryOptions.cs | 13 +- 3 files changed, 178 insertions(+), 134 deletions(-) diff --git a/RoboSharp.Extensions/RoboCommandPortable.cs b/RoboSharp.Extensions/RoboCommandPortable.cs index 07c499f4..ddeea303 100644 --- a/RoboSharp.Extensions/RoboCommandPortable.cs +++ b/RoboSharp.Extensions/RoboCommandPortable.cs @@ -1,23 +1,21 @@ - +#if NET6_0_OR_GREATER + using RoboSharp.EventArgObjects; using RoboSharp.Extensions.Helpers; using RoboSharp.Extensions.Options; using RoboSharp.Interfaces; using RoboSharp.Results; using System; -using System.CodeDom; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; -using System.ComponentModel.Design; using System.IO; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; + #nullable enable namespace RoboSharp.Extensions @@ -30,7 +28,7 @@ public class RoboCommandPortable : IRoboCommand, INotifyPropertyChanged { internal static void ThrowUnsupportedFrameworkException() { -#if !(NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER || NET8_0_OR_GREATER) +#if !(NET6_0_OR_GREATER) throw new System.NotSupportedException("This process relies on IAsyncEnumerable, which is not present for this framework."); #endif } @@ -54,8 +52,22 @@ public RoboCommandPortable(IFileCopierFactory fileCopierFactory, IAuthenticator? private readonly IFileCopierFactory copierFactory; private readonly IAuthenticator authenticator; + private readonly SemaphoreSlim _startLock = new SemaphoreSlim(1, 1); + + private string name = string.Empty; + private bool isPaused = false, isRunning = false, isScheduled = false, isCancelled = false, stopIfDisposing; + private CopyOptions? _CopyOptions; + private SelectionOptions? _SelectionOptions; + private RetryOptions? _RetryOptions; + private LoggingOptions? _LoggingOptions; + private JobOptions? _JobOptions; + private RoboSharpConfiguration? _Configuration; + private IProgressEstimator? progressEstimator; + private CancellationTokenSource? _CancellationTokenSource; + private RoboCopyResults? _lastResults; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -- Inherits descriptions from interface. -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public event RoboCommand.FileProcessedHandler? OnFileProcessed; public event RoboCommand.CommandErrorHandler? OnCommandError; public event RoboCommand.ErrorHandler? OnError; @@ -77,19 +89,6 @@ private void SetProperty(ref T field, T value, string name) System.Diagnostics.Debug.Assert(field?.Equals(value) ?? (value is null && field is null), "FactoryCommand.SetProperty failed to update field.", "Field {0} value was not updated to value [{1}]'", name, value); } - private string name = string.Empty; - private bool isPaused = false, isRunning = false, isScheduled = false, isCancelled = false, stopIfDisposing; - private CopyOptions _CopyOptions = new(); - private SelectionOptions _SelectionOptions = new(); - private RetryOptions _RetryOptions = new(); - private LoggingOptions _LoggingOptions = new(); - private JobOptions _JobOptions = new(); - private RoboSharpConfiguration _Configuration = new(); - private IProgressEstimator? progressEstimator; - private CancellationTokenSource? _CancellationTokenSource; - private SemaphoreSlim _startLock = new SemaphoreSlim(1, 1); - private RoboCopyResults? _lastResults; - public string Name { get => name; private set => SetProperty(ref name, value, nameof(Name)); } public bool IsPaused { get => isPaused; private set => SetProperty(ref isPaused, value, nameof(IsPaused)); } public bool IsRunning { get => isRunning; private set => SetProperty(ref isRunning, value, nameof(IsRunning)); } @@ -98,12 +97,13 @@ private void SetProperty(ref T field, T value, string name) public bool StopIfDisposing { get => stopIfDisposing; private set => SetProperty(ref stopIfDisposing, value, nameof(StopIfDisposing)); } public IProgressEstimator? IProgressEstimator { get => progressEstimator; private set => SetProperty(ref progressEstimator, value, nameof(IProgressEstimator)); } public string CommandOptions => GenerateParameters(); - public CopyOptions CopyOptions { get => _CopyOptions; set => SetProperty(ref _CopyOptions, value, nameof(CopyOptions)); } - public SelectionOptions SelectionOptions { get => _SelectionOptions; set => SetProperty(ref _SelectionOptions, value, nameof(SelectionOptions)); } - public RetryOptions RetryOptions { get => _RetryOptions; set => SetProperty(ref _RetryOptions, value, nameof(RetryOptions)); } - public LoggingOptions LoggingOptions { get => _LoggingOptions; set => SetProperty(ref _LoggingOptions, value, nameof(LoggingOptions)); } - public JobOptions JobOptions { get => _JobOptions; set => SetProperty(ref _JobOptions, value, nameof(JobOptions)); } - public RoboSharpConfiguration Configuration { get => _Configuration; set => SetProperty(ref _Configuration, value, nameof(Configuration)); } + + public CopyOptions CopyOptions { get => _CopyOptions ??= new(); set => SetProperty(ref _CopyOptions, value, nameof(CopyOptions)); } + public SelectionOptions SelectionOptions { get => _SelectionOptions ??= new(); set => SetProperty(ref _SelectionOptions, value, nameof(SelectionOptions)); } + public RetryOptions RetryOptions { get => _RetryOptions ??= new(); set => SetProperty(ref _RetryOptions, value, nameof(RetryOptions)); } + public LoggingOptions LoggingOptions { get => _LoggingOptions ??= new(); set => SetProperty(ref _LoggingOptions, value, nameof(LoggingOptions)); } + public JobOptions JobOptions { get => _JobOptions ??= new(); set => SetProperty(ref _JobOptions, value, nameof(JobOptions)); } + public RoboSharpConfiguration Configuration { get => _Configuration ??= new(); set => SetProperty(ref _Configuration, value, nameof(Configuration)); } public void Pause() @@ -210,18 +210,6 @@ void PostRunListOnlyAction() LoggingOptions.ListOnly = false; } - - -#if !(NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER || NET8_0_OR_GREATER) - - private Task Run(string domain, string username, string password, Action? preRunAction = null, Action? postRunAction = null) - { - ThrowUnsupportedFrameworkException(); - return Task.CompletedTask; - } - -#else - private Regex[] GetFileExclusionRegex() => excludedFiledRegex??= SelectionOptions.GetExcludedFileRegex(); private Regex[]? excludedFiledRegex; @@ -348,122 +336,108 @@ private async Task RunAsync(CancellationToken cancellationToken) resultsBuilder.AddDir(dirPair.ProcessedFileInfo); OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(dirPair.ProcessedFileInfo)); - if (includeEmpty) - dirPair.Destination.Create(); - - // ── 2a. Source files ────────────────────────────────────────────────── - - await foreach (IFileCopier copier in CreateFileCopiers(dirPair, cancellationToken)) + // ── Process Purge candidates (destination-only files) ──────────────────── + // ── Perform this first to clear space and also reduce run-time (avoid evaluating files that are copied into destination) + if (dirPair.Destination.Exists) { - cancellationToken.ThrowIfCancellationRequested(); - - // Evaluate populates copier.ProcessedFileInfo (FileClass, Size, Name) - // AND sets ShouldCopy / ShouldPurge based on this IRoboCommand's options. - EvaluateFilePair(copier); + await foreach (IFileCopier purgeCopier in CreatePurgeCandidates(dirPair, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); - ProcessedFileInfo fileInfo = copier.ProcessedFileInfo; + EvaluateFilePair(purgeCopier); + ProcessedFileInfo purgeInfo = purgeCopier.ProcessedFileInfo; - if (copier.ShouldCopy) - { - if (listOnly) + if (purgeCopier.ShouldPurge) { - OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); - progressReporter.AddFileCopied(fileInfo); - resultsBuilder.AddFileCopied(fileInfo); - } - else if (touchFiles) - { - dirPair.Destination.Create(); - if (copier.Destination.Exists is false) - copier.Destination.Create(); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(purgeInfo)); - progressReporter.AddFileCopied(fileInfo); - resultsBuilder.AddFileCopied(fileInfo); + try + { + purgeCopier.Destination.Delete(); + progressReporter.AddFileExtra(purgeInfo); + resultsBuilder.AddFilePurged(purgeInfo); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + resultsBuilder.AddFileFailed(purgeInfo); + OnCommandError?.Invoke(this, new CommandErrorEventArgs(ex.Message, ex)); + } } else { - await multiThreadedController.WaitAsync(cancellationToken); - - // Announce the file before the transfer (mirrors Robocopy's pre-copy log line) - OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); - runningTasks[copier] = Task.Run(async () => - { - try - { - Directory.CreateDirectory(dirPair.Destination.FullName); - copier.ProgressUpdated += RaiseProgressUpdated; - if (CopyOptions.MoveFiles || CopyOptions.MoveFilesAndDirectories) - await copier.MoveAsync(true, cancellationToken).ConfigureAwait(false); - else - await copier.CopyAsync(true, cancellationToken).ConfigureAwait(false); - - progressReporter.AddFileCopied(fileInfo); - resultsBuilder.AddFileCopied(fileInfo); - } - catch (OperationCanceledException) - { - throw; // let cancellation propagate cleanly - } - catch (Exception ex) - { - resultsBuilder.AddFileFailed(fileInfo); - OnError?.Invoke(this, new ErrorEventArgs(ex, copier.Destination.FullName, DateTime.Now)); - } - finally - { - copier.ProgressUpdated -= RaiseProgressUpdated; - runningTasks.TryRemove(copier, out _); - multiThreadedController.Release(); - } - }, cancellationToken); + // Extra file is present but purge is disabled — treat as skipped/extra + progressReporter.AddFileExtra(purgeInfo); + resultsBuilder.AddFileExtra(purgeInfo); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(purgeInfo)); } } - else + + if ((CopyOptions.Mirror || CopyOptions.Purge) && dirPair.IsExtra()) { - // File was evaluated but not copied (skipped/extra/same/newer/older). - // Still report it so consumers see the full picture. - progressReporter.AddFileSkipped(fileInfo); - resultsBuilder.AddFileSkipped(fileInfo); - OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); + dirPair.Destination.Delete(true); + continue; // source does not exist -> move to next dirpair } } - // ── 2b. Purge candidates (destination-only files) ──────────────────── - - await foreach (IFileCopier purgeCopier in CreatePurgeCandidates(dirPair, cancellationToken)) + // ── 2a. Source files ────────────────────────────────────────────────── + if (dirPair.Source.Exists) { - cancellationToken.ThrowIfCancellationRequested(); + if (includeEmpty) + dirPair.Destination.Create(); - EvaluateFilePair(purgeCopier); - ProcessedFileInfo purgeInfo = purgeCopier.ProcessedFileInfo; - - if (purgeCopier.ShouldPurge) + await foreach (IFileCopier copier in CreateFileCopiers(dirPair, cancellationToken)) { - OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(purgeInfo)); + cancellationToken.ThrowIfCancellationRequested(); - try - { - purgeCopier.Destination.Delete(); - progressReporter.AddFileExtra(purgeInfo); - resultsBuilder.AddFilePurged(purgeInfo); - } - catch (OperationCanceledException) + // Evaluate populates copier.ProcessedFileInfo (FileClass, Size, Name) + // AND sets ShouldCopy / ShouldPurge based on this IRoboCommand's options. + EvaluateFilePair(copier); + + ProcessedFileInfo fileInfo = copier.ProcessedFileInfo; + + if (copier.ShouldCopy) { - throw; + if (listOnly) + { + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); + progressReporter.AddFileCopied(fileInfo); + resultsBuilder.AddFileCopied(fileInfo); + } + else if (touchFiles) + { + dirPair.Destination.Create(); + if (copier.Destination.Exists is false) + copier.Destination.Create(); + + progressReporter.AddFileCopied(fileInfo); + resultsBuilder.AddFileCopied(fileInfo); + } + else + { + await multiThreadedController.WaitAsync(cancellationToken); + + // Announce the file before the transfer (mirrors Robocopy's pre-copy log line) + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); + var task = PerformCopyOrMove(dirPair, copier, progressReporter, resultsBuilder, multiThreadedController, runningTasks, cancellationToken); + + if (task.Status < TaskStatus.RanToCompletion) + runningTasks[copier] = task; + else + await task; // acknowledge completion + } } - catch (Exception ex) + else { - resultsBuilder.AddFileFailed(purgeInfo); - OnCommandError?.Invoke(this, new CommandErrorEventArgs(ex.Message, ex)); + // File was evaluated but not copied (skipped/extra/same/newer/older). + progressReporter.AddFileSkipped(fileInfo); + resultsBuilder.AddFileSkipped(fileInfo); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); } } - else - { - // Extra file is present but purge is disabled — treat as skipped/extra - progressReporter.AddFileExtra(purgeInfo); - resultsBuilder.AddFileExtra(purgeInfo); - OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(purgeInfo)); - } } } @@ -484,6 +458,54 @@ private async Task RunAsync(CancellationToken cancellationToken) // ── Helpers ────────────────────────────────────────────────────────────────── + private async Task PerformCopyOrMove( + DirectoryPair dirPair, + IFileCopier copier, + Results.ProgressEstimator progressReporter, + ResultsBuilder resultsBuilder, + SemaphoreSlim multiThreadedController, + ConcurrentDictionary runningTasks, + CancellationToken cancellationToken) + { + bool success = false; + int tries = 0; + int maxTries = RetryOptions.RetryCount <= 1 ? 1 : RetryOptions.RetryCount; + TimeSpan retryWaitTime = maxTries > 1 ? RetryOptions.GetRetryWaitTime() : TimeSpan.Zero; + + while (success == false && tries < maxTries) + { + tries++; + try + { + Directory.CreateDirectory(dirPair.Destination.FullName); + copier.ProgressUpdated += RaiseProgressUpdated; + if (CopyOptions.MoveFiles || CopyOptions.MoveFilesAndDirectories) + await copier.MoveAsync(true, cancellationToken).ConfigureAwait(false); + else + await copier.CopyAsync(true, cancellationToken).ConfigureAwait(false); + success = true; + progressReporter.AddFileCopied(copier.ProcessedFileInfo); + resultsBuilder.AddFileCopied(copier.ProcessedFileInfo); + } + catch (OperationCanceledException) + { + throw; // let cancellation propagate cleanly + } + catch (Exception ex) when (success == false) // don't catch errors from the progress reporter or results builder. + { + resultsBuilder.AddFileFailed(copier.ProcessedFileInfo); + OnError?.Invoke(this, new ErrorEventArgs(ex, copier.Destination.FullName, DateTime.Now)); + await Task.Delay(retryWaitTime, cancellationToken).ConfigureAwait(false); + } + finally + { + copier.ProgressUpdated -= RaiseProgressUpdated; + runningTasks.TryRemove(copier, out _); + multiThreadedController.Release(); + } + } + } + /// /// Yields the root pair and (if recurse is true) all sub-directory pairs, /// mirroring Robocopy's directory tree walk. @@ -574,6 +596,6 @@ private async IAsyncEnumerable CreatePurgeCandidates(IDirectoryPair yield return copierFactory.Create(sourceFile, destFile, dirPair); } } -#endif } } +#endif \ No newline at end of file diff --git a/RoboSharp.Extensions/RoboCommandPortableFactory.cs b/RoboSharp.Extensions/RoboCommandPortableFactory.cs index 3050da34..10a2ef05 100644 --- a/RoboSharp.Extensions/RoboCommandPortableFactory.cs +++ b/RoboSharp.Extensions/RoboCommandPortableFactory.cs @@ -3,6 +3,8 @@ #nullable enable +#if NET6_0_OR_GREATER + namespace RoboSharp.Extensions { /// @@ -11,6 +13,13 @@ namespace RoboSharp.Extensions /// public class RoboCommandPortableFactory : IRoboCommandFactory { + /// + /// Gets a that uses the + /// + /// + /// + public static IRoboCommandFactory GetStreamedCopierFactory(IAuthenticator? authenticator = null) => new RoboCommandPortableFactory(StreamedCopierFactory.DefaultFactory, authenticator); + /// /// Create a new to produce objects /// @@ -67,4 +76,6 @@ public IRoboCommand GetRoboCommand(string source, string destination, CopyAction return cmd; } } -} \ No newline at end of file +} + +#endif \ No newline at end of file diff --git a/RoboSharp/RetryOptions.cs b/RoboSharp/RetryOptions.cs index 2a30f7ef..c86ac5ec 100644 --- a/RoboSharp/RetryOptions.cs +++ b/RoboSharp/RetryOptions.cs @@ -95,7 +95,7 @@ internal string Parse() if (RetryCount >= 0 && RetryCount != 1000000) options.AppendFormat(RETRY_COUNT, RetryCount); - + if (RetryWaitTime >= 0 && RetryWaitTime != 30) options.AppendFormat(RETRY_WAIT_TIME, RetryWaitTime); @@ -129,5 +129,16 @@ public void Merge(RetryOptions options) WaitForSharenames |= options.WaitForSharenames; SaveToRegistry |= options.SaveToRegistry; } + + /// + /// Gets a representing the + /// + /// + public TimeSpan GetRetryWaitTime() + { + if (RetryWaitTime <= 0) + return TimeSpan.Zero; + return TimeSpan.FromSeconds(RetryWaitTime); + } } } From 5c6f934721442ddfd28a0ae58e6d12fc5f2558b7 Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:19:52 -0400 Subject: [PATCH 07/16] Update Nuget Package for MSTest & Implement Testing Timeouts --- .../AssertExtensions.cs | 34 ----- .../BatchCommandTests.cs | 8 +- .../Helpers/ResultsBuilderTests.cs | 2 + .../IFileCopierTests.cs | 32 ++-- .../RoboCommandPortableTests.cs | 141 ++++++++---------- .../RoboMoverTests.cs | 112 +++++++------- .../RoboSharp.Extensions.UnitTests.csproj | 2 +- RoboSharp.Extensions.UnitTests/TestPrep.cs | 84 ++++++++--- .../Windows/CopyFileExTests.cs | 55 ++++--- .../ProgressEstimatorTests.cs | 68 +++++---- .../RoboCommandParserTests.cs | 2 +- .../RoboSharpUnitTesting.csproj | 2 +- RoboSharpUnitTesting/SelectionOptionsTests.cs | 2 +- RoboSharpUnitTesting/Test_Setup.cs | 5 +- 14 files changed, 278 insertions(+), 271 deletions(-) delete mode 100644 RoboSharp.Extensions.UnitTests/AssertExtensions.cs diff --git a/RoboSharp.Extensions.UnitTests/AssertExtensions.cs b/RoboSharp.Extensions.UnitTests/AssertExtensions.cs deleted file mode 100644 index 2b5ac1d9..00000000 --- a/RoboSharp.Extensions.UnitTests/AssertExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace RoboSharp.Extensions.Tests -{ - public static class AssertExtensions - { - /// - /// Allows catching derived types - Meant for OperationCancelledException - /// - public static async Task AssertThrowsExceptionAsync(this Task task, string message = "") where T : Exception - { - try { await task; } - catch (T) { return; } - catch (Exception e) { Assert.ThrowsException(() => throw e, message); } - Assert.ThrowsException(() => { }, message); - } - - /// - /// Allows catching derived types - Meant for OperationCancelledException - /// - public static async Task AssertThrowsExceptionAsync(this Func task, string message = "") where T : Exception - { - try { await task(); } - catch (T) { return; } - catch (Exception e) { Assert.ThrowsException(() => throw e, message); } - Assert.ThrowsException(() => { }, message); - } - } -} diff --git a/RoboSharp.Extensions.UnitTests/BatchCommandTests.cs b/RoboSharp.Extensions.UnitTests/BatchCommandTests.cs index 0b0393af..1af875a7 100644 --- a/RoboSharp.Extensions.UnitTests/BatchCommandTests.cs +++ b/RoboSharp.Extensions.UnitTests/BatchCommandTests.cs @@ -16,7 +16,10 @@ namespace RoboSharp.Extensions.Tests [TestClass] public class BatchCommandTests { + public TestContext TestContext { get; set; } + [TestMethod] + [Timeout(5000, CooperativeCancellation = true)] public async Task TestCopyOperation() { string destination = TestPrep.GetRandomPath(true); @@ -30,7 +33,7 @@ public async Task TestCopyOperation() cmd.LoggingOptions.IncludeFullPathNames = true; cmd.Configuration.EnableFileLogging = true; cmd.AddCopiers(files); - var results = await Test_Setup.RunTest(cmd); + var results = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); Test_Setup.WriteLogLines(results.Results); Assert.AreEqual(files.Count(), results.Results.FilesStatistic.Copied); // expect 4 } @@ -41,6 +44,7 @@ public async Task TestCopyOperation() } [TestMethod] + [Timeout(5000, CooperativeCancellation =true)] public async Task TestCancellation() { CancellationTokenSource cToken = new CancellationTokenSource(); @@ -62,7 +66,7 @@ public async Task TestCancellation() }; cmd.OnError += (o, e) => Console.WriteLine(e.Error); - var results = await Test_Setup.RunTest(cmd); + var results = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); Test_Setup.WriteLogLines(results.Results); Assert.IsTrue(results.Results.Status.WasCancelled, "Results.Status.WasCancelled flag not set!"); var numCopied = results.Results.FilesStatistic.Copied; diff --git a/RoboSharp.Extensions.UnitTests/Helpers/ResultsBuilderTests.cs b/RoboSharp.Extensions.UnitTests/Helpers/ResultsBuilderTests.cs index de14b6bf..53ccfe13 100644 --- a/RoboSharp.Extensions.UnitTests/Helpers/ResultsBuilderTests.cs +++ b/RoboSharp.Extensions.UnitTests/Helpers/ResultsBuilderTests.cs @@ -135,9 +135,11 @@ public void AddFileSkippedTest() var testFile = new ProcessedFileInfo() { FileClass = cmd.Configuration.LogParsing_NewFile, FileClassType = FileClassType.File, Name = "TestFileName", Size = 100 }; cmd.LoggingOptions.NoFileList = true; + cmd.LoggingOptions.ReportExtraFiles = false; builder.AddFileSkipped(testFile); Assert.AreEqual(0, builder.CurrentLogLines.LongLength); cmd.LoggingOptions.NoFileList = false; + cmd.LoggingOptions.ReportExtraFiles = true; builder.AddFileSkipped(testFile); Assert.AreEqual(1, builder.CurrentLogLines.LongLength); Assert.AreEqual(2, builder.GetResults().FilesStatistic.Skipped); diff --git a/RoboSharp.Extensions.UnitTests/IFileCopierTests.cs b/RoboSharp.Extensions.UnitTests/IFileCopierTests.cs index d5b433ba..8f56c3ac 100644 --- a/RoboSharp.Extensions.UnitTests/IFileCopierTests.cs +++ b/RoboSharp.Extensions.UnitTests/IFileCopierTests.cs @@ -90,7 +90,7 @@ public static async Task ThrowsIfNotWindowsPlatform(IFileCopier copier) { if (attr.PlatformName.StartsWith("windows")) { - await Assert.ThrowsExceptionAsync(copier.CopyAsync, "\r\n failed to throw PlatformNotSupported"); + await Assert.ThrowsAsync(copier.CopyAsync, "\r\n failed to throw PlatformNotSupported"); return false; } } @@ -115,7 +115,7 @@ internal static async Task RunTests(IFileCopierFactory factory) /// /// Tests the basic functionality of an /// - [DynamicData(nameof(GetCopierFactory), dynamicDataSourceType:DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetCopierName))] + [DynamicData(nameof(GetCopierFactory), DynamicDataDisplayName = nameof(GetCopierName))] [TestMethod] public void RunFactoryTests(IFileCopierFactory factory) { @@ -147,7 +147,7 @@ public void RunFactoryTests(IFileCopierFactory factory) /// /// Tests the basic functionality of an /// - [DynamicData(nameof(GetCopier), dynamicDataSourceType: DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetCopierName))] + [DynamicData(nameof(GetCopier), DynamicDataDisplayName = nameof(GetCopierName))] [TestMethod] public async Task CopyAsyncTest(IFileCopier copier) { @@ -163,7 +163,7 @@ public async Task CopyAsyncTest(IFileCopier copier) if (await ThrowsIfNotWindowsPlatform(copier) is false) return; //Source is missing - await Assert.ThrowsExceptionAsync(copier.CopyAsync, "\n --Did not throw when source is missing \n"); + await Assert.ThrowsAsync(copier.CopyAsync, "\n --Did not throw when source is missing \n"); PrepSourceAndDest(copier); @@ -173,22 +173,22 @@ public async Task CopyAsyncTest(IFileCopier copier) Assert.IsTrue(await copier.CopyAsync(true, CancellationToken.None), "\n -- IFileCopierTests - Copy - Test 3\n"); //File already exists - await Assert.ThrowsExceptionAsync(() => copier.CopyAsync(), "\n -- IFileCopierTests - Prevent Overwrite - Test 1\n"); - await Assert.ThrowsExceptionAsync(() => copier.CopyAsync(false), "\n -- IFileCopierTests - Prevent Overwrite - Test 2\n"); - await Assert.ThrowsExceptionAsync(() => copier.CopyAsync(false, CancellationToken.None), "\n -- IFileCopierTests - Prevent Overwrite - Test 3\n"); + await Assert.ThrowsAsync(() => copier.CopyAsync(), "\n -- IFileCopierTests - Prevent Overwrite - Test 1\n"); + await Assert.ThrowsAsync(() => copier.CopyAsync(false), "\n -- IFileCopierTests - Prevent Overwrite - Test 2\n"); + await Assert.ThrowsAsync(() => copier.CopyAsync(false, CancellationToken.None), "\n -- IFileCopierTests - Prevent Overwrite - Test 3\n"); await Cleanup(copier, false); // Cancellation Test 1 -- BEFORE start of the operation CancellationTokenSource cToken = new CancellationTokenSource(); cToken.Cancel(); - await AssertExtensions.AssertThrowsExceptionAsync(async () => copyResult = await copier.CopyAsync(true, cToken.Token), "\n -- Cancellation Token Test (1)\n"); + await Assert.ThrowsAsync(async () => copyResult = await copier.CopyAsync(true, cToken.Token), "\n -- Cancellation Token Test (1)\n"); Assert.IsFalse(File.Exists(destPath), "\nCancelled operation did not delete destination file (1)"); Assert.IsFalse(copier.Destination.Exists, "\nDestination object was not refreshed (1)"); // Cancellation Test 2 -- Mid-Write + tests ProgressUpdated copier.ProgressUpdated += CancelEventHandler; - await AssertExtensions.AssertThrowsExceptionAsync(async () => copyResult = await copier.CopyAsync(true, CancellationToken.None), "\n -- Copier.Cancel() Test (2)\n"); + await Assert.ThrowsAsync(async () => copyResult = await copier.CopyAsync(true, CancellationToken.None), "\n -- Copier.Cancel() Test (2)\n"); Assert.IsFalse(File.Exists(destPath), "\nCancelled operation did not delete destination file (2)"); Assert.IsFalse(copier.Destination.Exists, "\nDestination object was not refreshed (2)"); copier.ProgressUpdated -= CancelEventHandler; @@ -197,7 +197,7 @@ public async Task CopyAsyncTest(IFileCopier copier) cToken = new CancellationTokenSource(); void tokenHandler(object o, EventArgs e) => cToken.Cancel(); copier.ProgressUpdated += tokenHandler; - await AssertExtensions.AssertThrowsExceptionAsync(async () => copyResult = await copier.CopyAsync(true, cToken.Token), "\n -- Cancellation Test 3\n"); + await Assert.ThrowsAsync(async () => copyResult = await copier.CopyAsync(true, cToken.Token), "\n -- Cancellation Test 3\n"); Assert.IsFalse(File.Exists(destPath), "\nCancelled operation did not delete destination file (3)"); Assert.IsFalse(copier.Destination.Exists, "\nDestination object was not refreshed (3)"); copier.ProgressUpdated -= tokenHandler; @@ -254,7 +254,7 @@ void CancelEventHandler(object sender, EventArgs e) /// /// Tests the basic functionality of an /// - [DynamicData(nameof(GetCopier), dynamicDataSourceType: DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetCopierName))] + [DynamicData(nameof(GetCopier), DynamicDataDisplayName = nameof(GetCopierName))] [TestMethod] public async Task MoveAsyncTest(IFileCopier copier) { @@ -266,7 +266,7 @@ public async Task MoveAsyncTest(IFileCopier copier) if (await ThrowsIfNotWindowsPlatform(copier) is false) return; //Source is missing - await Assert.ThrowsExceptionAsync(() => copier.MoveAsync(), "\n --Did not throw when source is missing \n"); + await Assert.ThrowsAsync(() => copier.MoveAsync(), "\n --Did not throw when source is missing \n"); const string fileNotMoved = "\n -- IFileCopierTests - Move - Source Not Moved - Test {0}\n"; const string fileMoved = "\n -- IFileCopierTests - Move - Source Not Moved - Test {0}\n"; @@ -286,13 +286,13 @@ public async Task MoveAsyncTest(IFileCopier copier) //File already exists PrepSourceAndDest(copier, false); - await Assert.ThrowsExceptionAsync(() => copier.MoveAsync(), "\n -- IFileCopierTests - Move - Prevent Overwrite - Test 1\n"); + await Assert.ThrowsAsync(() => copier.MoveAsync(), "\n -- IFileCopierTests - Move - Prevent Overwrite - Test 1\n"); Assert.IsTrue(File.Exists(copier.Source.FullName), string.Format(fileMoved, 1)); - await Assert.ThrowsExceptionAsync(() => copier.MoveAsync(false), "\n -- IFileCopierTests - Move - Prevent Overwrite - Test 2\n"); + await Assert.ThrowsAsync(() => copier.MoveAsync(false), "\n -- IFileCopierTests - Move - Prevent Overwrite - Test 2\n"); Assert.IsTrue(File.Exists(copier.Source.FullName), string.Format(fileMoved, 2)); - await Assert.ThrowsExceptionAsync(() => copier.MoveAsync(false, CancellationToken.None), "\n -- IFileCopierTests - Move - Prevent Overwrite - Test 3\n"); + await Assert.ThrowsAsync(() => copier.MoveAsync(false, CancellationToken.None), "\n -- IFileCopierTests - Move - Prevent Overwrite - Test 3\n"); Assert.IsTrue(File.Exists(copier.Source.FullName), string.Format(fileMoved, 3)); await Cleanup(copier, false); } @@ -313,7 +313,7 @@ public async Task MoveAsyncTest(IFileCopier copier) /// /// Tests that attributes and file itself are copied to the destination file properly, just like if they were copied via File.CopyTo(); /// - [DynamicData(nameof(GetCopier), dynamicDataSourceType: DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetCopierName))] + [DynamicData(nameof(GetCopier), DynamicDataDisplayName = nameof(GetCopierName))] [TestMethod] public async Task AttributesCopiedProperlyTest(IFileCopier copier) { diff --git a/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs b/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs index 04f470ad..f0304c12 100644 --- a/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs +++ b/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs @@ -8,9 +8,10 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER namespace RoboSharp.Extensions.Tests { @@ -41,7 +42,13 @@ protected override IRoboCommand GenerateCommand(bool UseLargerFileSet, bool List [TestClass] public class RoboCommandPortable_Tests { + /// + /// Set by testing framework + /// + public TestContext TestContext { get; set; } + const LoggingFlags DefaultLoggingAction = LoggingFlags.RoboSharpDefault | LoggingFlags.NoJobHeader; + const LoggingFlags ListOnlyLoggingAction = LoggingFlags.RoboSharpDefault | LoggingFlags.NoJobHeader | LoggingFlags.ListOnly; private static RoboCommandPortable GetCommand(RoboCommand rc, IFileCopierFactory factory = null) { @@ -55,16 +62,11 @@ private static RoboCommandPortable GetCommand(RoboCommand rc, IFileCopierFactory }; } - static string GetMoveSource() - { - string original = TestPrep.SourceDirPath; - return Path.Combine(original.Replace(Path.GetFileName(original), ""), "MoveSource"); - } + [TestMethod] + [Timeout(1000, CooperativeCancellation = true)] [DataRow(true, @"C:\SomeDir")] [DataRow(false, @"D:\System Volume Information")] - [TestMethod] - [Timeout(10000)] public void IsAllowedDir(bool expected, string path) { Assert.AreEqual(expected, RoboMover.IsAllowedRootDirectory(new DirectoryInfo(path))); @@ -74,51 +76,53 @@ public void IsAllowedDir(bool expected, string path) /// Copy Test will use a standard ROBOCOPY command /// [TestMethod] - [Timeout(30000)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Mirror }, DisplayName = "Mirror")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Default }, DisplayName = "Defaults")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectories }, DisplayName = "Subdirectories")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectoriesIncludingEmpty }, DisplayName = "EmptySubdirectories")] + [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.Mirror }, DisplayName = "ListOnly-Mirror")] + [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.Default }, DisplayName = "ListOnly-Defaults")] + [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectories }, DisplayName = "ListOnly-Subdirectories")] + [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectoriesIncludingEmpty }, DisplayName = "ListOnly-EmptySubdirectories")] public async Task CopyTest(object[] flags) { - Test_Setup.ClearOutTestDestination(); - CopyActionFlags copyAction = (CopyActionFlags)flags[2]; - SelectionFlags selectionFlags = (SelectionFlags)flags[1]; - LoggingFlags loggingAction = (LoggingFlags)flags[0]; + try + { + Test_Setup.ClearOutTestDestination(); + CopyActionFlags copyAction = (CopyActionFlags)flags[2]; + SelectionFlags selectionFlags = (SelectionFlags)flags[1]; + LoggingFlags loggingAction = (LoggingFlags)flags[0]; - var rc = TestPrep.GetRoboCommand(false, copyAction, selectionFlags, loggingAction); - var crc = GetCommand(rc); + var rc = TestPrep.GetRoboCommand(false, copyAction, selectionFlags, loggingAction); + var crc = GetCommand(rc); - rc.LoggingOptions.ListOnly = true; - var results1 = await TestPrep.RunTests(rc, crc, false); - TestPrep.CompareTestResults(results1[0], results1[1], rc.LoggingOptions.ListOnly); + Assert.AreEqual(loggingAction.HasFlag(LoggingFlags.ListOnly), rc.LoggingOptions.ListOnly); + Assert.AreEqual(loggingAction.HasFlag(LoggingFlags.ListOnly), crc.LoggingOptions.ListOnly); + + TestContext.CancellationToken.Register(() => + { + rc.Stop(); + crc.Stop(); + }); - rc.LoggingOptions.ListOnly = false; - var results2 = await TestPrep.RunTests(rc, crc, true); - TestPrep.CompareTestResults(results2[0], results2[1], rc.LoggingOptions.ListOnly); + var results1 = await TestPrep.RunTests(rc, crc, !rc.LoggingOptions.ListOnly, TestContext.CancellationToken); + TestPrep.CompareTestResults(results1[0], results1[1], rc.LoggingOptions.ListOnly); + } + catch (OperationCanceledException) when (TestContext.CancellationToken.IsCancellationRequested) + { } } + + private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags selectionFlags, LoggingFlags loggingFlags, out RoboCommand rc, out RoboCommandPortable rm) { rc = TestPrep.GetRoboCommand(false, copyFlags, selectionFlags, loggingFlags); - rc.CopyOptions.Source = GetMoveSource(); + rc.CopyOptions.Source = TestPrep.GetMoveSource(); rm = GetCommand(rc, Mocks.MockFileCopierFactory.Instance); } - private static async Task PrepMoveFiles() - { - var rc = TestPrep.GetRoboCommand(false, CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction); - rc.CopyOptions.Destination = GetMoveSource(); - Directory.CreateDirectory(rc.CopyOptions.Destination); - await rc.Start(); - var results = rc.GetResults(); - if (results.RoboCopyErrors.Length > 0) - throw new Exception( - "Prep Failed \n" + - string.Concat(args: results.RoboCopyErrors.Select(e => "\n RoboCommandError :\t" + e.GetType() + "\t" + e.ErrorDescription + "\t:\t" + e.ErrorPath).ToArray()) + - "\n" - ); - } + private const CopyActionFlags Mov_ = CopyActionFlags.MoveFiles; private const CopyActionFlags Move = CopyActionFlags.MoveFilesAndDirectories; @@ -127,7 +131,7 @@ private static async Task PrepMoveFiles() /// This uses the actual logic provided by the RoboMover object /// [TestMethod] - [Timeout(10000)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -141,12 +145,12 @@ public async Task MoveTest(object[] flags) if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, TestPrep.PrepMoveFiles, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } [TestMethod] - [Timeout(10000)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -157,12 +161,12 @@ public async Task FileInclusionTest(object[] flags) //CopyActionFlags copyAction GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; rc.CopyOptions.FileFilter = new string[] { "*.txt" }; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, TestPrep.PrepMoveFiles, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } [TestMethod] - [Timeout(10000)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -174,13 +178,13 @@ public async Task FileExclusionTest(object[] flags) //CopyActionFlags copyAction rc.SelectionOptions.ExcludedFiles.Add("*.txt"); rc.Configuration.EnableFileLogging = true; bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, TestPrep.PrepMoveFiles, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } [TestMethod] - [Timeout(10000)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(data: new object[] { Move | CopyActionFlags.CopySubdirectories, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Include Subdirectories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] @@ -191,12 +195,12 @@ public async Task ExtraFileTest(object[] flags) //CopyActionFlags copyAction, Se if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2] | LoggingFlags.ReportExtraFiles, out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); - static async Task CreateFile() + static async Task CreateFile(CancellationToken token) { - await PrepMoveFiles(); + await TestPrep.PrepMoveFiles(token); string path = Path.Combine(TestPrep.DestDirPath, "ExtraFileTest.txt"); if (!File.Exists(path)) { @@ -207,7 +211,7 @@ static async Task CreateFile() } [TestMethod] - [Timeout(10000)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -217,12 +221,12 @@ public async Task SameFileTest(object[] flags) //CopyActionFlags copyAction, Sel if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); - static async Task CreateFile() + static async Task CreateFile(CancellationToken token) { - await PrepMoveFiles(); + await TestPrep.PrepMoveFiles(token); Directory.CreateDirectory(TestPrep.DestDirPath); string fn = "1024_Bytes.txt"; string dest = Path.Combine(TestPrep.DestDirPath, fn); @@ -232,7 +236,7 @@ static async Task CreateFile() } [TestMethod] - [Timeout(10000)] + [Timeout(5000, CooperativeCancellation = true)] // purge all [DataRow(0, true, Mov_)] [DataRow(0, true, Move)] @@ -266,11 +270,11 @@ public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, L GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.Depth = depth; - await RunPurge(cmd, mover); + await RunPurge(cmd, mover, TestContext.CancellationToken); } [TestMethod] - [Timeout(10000)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(true, Mov_)] [DataRow(false, Move)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories)] @@ -282,11 +286,11 @@ public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedFiles.Add("*0*_Bytes.txt"); - await RunPurge(cmd, mover); + await RunPurge(cmd, mover, TestContext.CancellationToken); } [TestMethod] - [Timeout(10000)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(true, Mov_)] [DataRow(true, Move)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories)] @@ -300,11 +304,11 @@ public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder1"); // Top level empty cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder4"); // Bottom level empty cmd.SelectionOptions.ExcludedDirectories.Add("SubFolder_2a"); // folder with contents - await RunPurge(cmd, mover); + await RunPurge(cmd, mover, TestContext.CancellationToken); } [TestMethod] - [Timeout(10000)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(true, Mov_)] [DataRow(true, Move)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories)] @@ -323,32 +327,17 @@ public async Task Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, Logg GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.FileFilter = new string[] { "*0*_Bytes.txt" }; - await RunPurge(cmd, mover); + await RunPurge(cmd, mover, TestContext.CancellationToken); } - private async Task RunPurge(RoboCommand cmd, RoboCommandPortable mover) + private static async Task RunPurge(RoboCommand cmd, RoboCommandPortable mover, CancellationToken token) { //if (Test_Setup.IsRunningOnAppVeyor()) return; - var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, CreateFilesToPurge); + var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, TestPrep.CreateFilesToPurge, token); TestPrep.CompareTestResults(results[0], results[1], cmd.LoggingOptions.ListOnly); } - [TestMethod] - [Timeout(10000)] - public async Task CreateFilesToPurge() - { - await PrepMoveFiles(); - RoboCommand prep = new RoboCommand(); - prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_1"); - prep.CopyOptions.Destination = Path.Combine(Test_Setup.TestDestination, "SubFolder_3"); - prep.CopyOptions.ApplyActionFlags(CopyActionFlags.CopySubdirectoriesIncludingEmpty); - Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder1", "EmptyFolder2")); - await prep.Start(); - prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_2"); - prep.CopyOptions.Destination = Path.Combine(prep.CopyOptions.Destination, "SubFolder_2a"); - await prep.Start(); - Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder3", "EmptyFolder4")); - } + } } #endif \ No newline at end of file diff --git a/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs b/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs index 72d6a1dc..0153f45a 100644 --- a/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs +++ b/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace RoboSharp.Extensions.Tests @@ -24,7 +25,13 @@ protected override IRoboCommand GenerateCommand(bool UseLargerFileSet, bool List [TestClass] public class RoboMoverTests { + /// + /// Set by testing framework + /// + public TestContext TestContext { get; set; } + const LoggingFlags DefaultLoggingAction = LoggingFlags.RoboSharpDefault | LoggingFlags.NoJobHeader; + const LoggingFlags ListOnlyLoggingAction = LoggingFlags.RoboSharpDefault | LoggingFlags.NoJobHeader | LoggingFlags.ListOnly; static string GetMoveSource() { @@ -44,10 +51,15 @@ public void IsAllowedDir(bool expected, string path) /// Copy Test will use a standard ROBOCOPY command /// [TestMethod] + [Timeout(10000, CooperativeCancellation =true)] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Mirror }, DisplayName = "Mirror")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Default }, DisplayName = "Defaults")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectories }, DisplayName = "Subdirectories")] [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectoriesIncludingEmpty }, DisplayName = "EmptySubdirectories")] + [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.Mirror }, DisplayName = "ListOnly-Mirror")] + [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.Default }, DisplayName = "ListOnly-Defaults")] + [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectories }, DisplayName = "ListOnly-Subdirectories")] + [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectoriesIncludingEmpty }, DisplayName = "ListOnly-EmptySubdirectories")] public async Task CopyTest(object[] flags) { Test_Setup.ClearOutTestDestination(); @@ -58,13 +70,18 @@ public async Task CopyTest(object[] flags) var rc = TestPrep.GetRoboCommand(false, copyAction, selectionFlags, loggingAction); var crc = TestPrep.GetIRoboCommand(rc); - rc.LoggingOptions.ListOnly = true; - var results1 = await TestPrep.RunTests(rc, crc, false); - TestPrep.CompareTestResults(results1[0], results1[1], rc.LoggingOptions.ListOnly); - rc.LoggingOptions.ListOnly = false; - var results2 = await TestPrep.RunTests(rc, crc, true); - TestPrep.CompareTestResults(results2[0], results2[1], rc.LoggingOptions.ListOnly); + Assert.AreEqual(loggingAction.HasFlag(LoggingFlags.ListOnly), rc.LoggingOptions.ListOnly); + Assert.AreEqual(loggingAction.HasFlag(LoggingFlags.ListOnly), crc.LoggingOptions.ListOnly); + + TestContext.CancellationToken.Register(() => + { + rc.Stop(); + crc.Stop(); + }); + + var results1 = await TestPrep.RunTests(rc, crc, CleanBetweenRuns: !rc.LoggingOptions.ListOnly, TestContext.CancellationToken); + TestPrep.CompareTestResults(results1[0], results1[1], rc.LoggingOptions.ListOnly); } private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags selectionFlags, LoggingFlags loggingFlags, out RoboCommand rc, out RoboMover rm) @@ -74,21 +91,6 @@ private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags se rm = TestPrep.GetIRoboCommand(rc); } - private static async Task PrepMoveFiles() - { - var rc = TestPrep.GetRoboCommand(false, CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, DefaultLoggingAction); - rc.CopyOptions.Destination = GetMoveSource(); - Directory.CreateDirectory(rc.CopyOptions.Destination); - await rc.Start(); - var results = rc.GetResults(); - if (results.RoboCopyErrors.Length > 0) - throw new Exception( - "Prep Failed \n" + - string.Concat(args: results.RoboCopyErrors.Select(e => "\n RoboCommandError :\t" + e.GetType() + "\t" + e.ErrorDescription + "\t:\t" + e.ErrorPath).ToArray()) + - "\n" - ); - } - private const CopyActionFlags Mov_ = CopyActionFlags.MoveFiles; private const CopyActionFlags Move = CopyActionFlags.MoveFilesAndDirectories; @@ -96,6 +98,7 @@ private static async Task PrepMoveFiles() /// This uses the actual logic provided by the RoboMover object /// [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -109,11 +112,12 @@ public async Task MoveTest(object[] flags) if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, TestPrep.PrepMoveFiles, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -124,11 +128,12 @@ public async Task FileInclusionTest(object[] flags) //CopyActionFlags copyAction GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; rc.CopyOptions.FileFilter = new string[] { "*.txt" }; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, TestPrep.PrepMoveFiles, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -140,12 +145,13 @@ public async Task FileExclusionTest(object[] flags) //CopyActionFlags copyAction rc.SelectionOptions.ExcludedFiles.Add("*.txt"); rc.Configuration.EnableFileLogging = true; bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, PrepMoveFiles); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, TestPrep.PrepMoveFiles, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); } [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] [DataRow(data: new object[] { Move | CopyActionFlags.CopySubdirectories, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Include Subdirectories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] @@ -156,12 +162,13 @@ public async Task ExtraFileTest(object[] flags) //CopyActionFlags copyAction, Se if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2] | LoggingFlags.ReportExtraFiles, out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); - static async Task CreateFile() + static async Task CreateFile(CancellationToken token) { - await PrepMoveFiles(); + await TestPrep.PrepMoveFiles(token); + token.ThrowIfCancellationRequested(); string path = Path.Combine(TestPrep.DestDirPath, "ExtraFileTest.txt"); if (!File.Exists(path)) { @@ -172,6 +179,7 @@ static async Task CreateFile() } [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -181,12 +189,13 @@ public async Task SameFileTest(object[] flags) //CopyActionFlags copyAction, Sel if (Test_Setup.IsRunningOnAppVeyor()) return; GetMoveCommands((CopyActionFlags)flags[0], (SelectionFlags)flags[0], (LoggingFlags)flags[2], out var rc, out var rm); bool listOnly = rc.LoggingOptions.ListOnly; - var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile); + var results1 = await TestPrep.RunTests(rc, rm, !listOnly, CreateFile, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], listOnly); - static async Task CreateFile() + static async Task CreateFile(CancellationToken token) { - await PrepMoveFiles(); + await TestPrep.PrepMoveFiles(token); + token.ThrowIfCancellationRequested(); Directory.CreateDirectory(TestPrep.DestDirPath); string fn = "1024_Bytes.txt"; string dest = Path.Combine(TestPrep.DestDirPath, fn); @@ -223,37 +232,40 @@ static async Task CreateFile() [DataRow(2, true, Mov_ | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] [DataRow(2, true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.Depth = depth; - await RunPurge(cmd, mover); + await RunPurge(cmd, mover, TestContext.CancellationToken); } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] [DataRow(true, Mov_)] [DataRow(false, Move)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories)] [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] [DataRow(false, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(false, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] - [TestMethod] public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedFiles.Add("*0*_Bytes.txt"); - await RunPurge(cmd, mover); + await RunPurge(cmd, mover, TestContext.CancellationToken); } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] [DataRow(true, Mov_)] [DataRow(true, Move)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories)] [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] - [TestMethod] public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); @@ -261,9 +273,11 @@ public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder1"); // Top level empty cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder4"); // Bottom level empty cmd.SelectionOptions.ExcludedDirectories.Add("SubFolder_2a"); // folder with contents - await RunPurge(cmd, mover); + await RunPurge(cmd, mover, TestContext.CancellationToken); } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] [DataRow(true, Mov_)] [DataRow(true, Move)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectories)] @@ -276,28 +290,28 @@ public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) [DataRow(true, Move | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] - [TestMethod] public async Task Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.FileFilter = new string[] { "*0*_Bytes.txt" }; - await RunPurge(cmd, mover); + await RunPurge(cmd, mover, TestContext.CancellationToken); } - private async Task RunPurge(RoboCommand cmd, RoboMover mover) + private async Task RunPurge(RoboCommand cmd, RoboMover mover, CancellationToken token) { //if (Test_Setup.IsRunningOnAppVeyor()) return; - var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, CreateFilesToPurge); + var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, TestPrep.CreateFilesToPurge, token); TestPrep.CompareTestResults(results[0], results[1], cmd.LoggingOptions.ListOnly); } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] [DataRow(CopyActionFlags.MoveFiles)] [DataRow(CopyActionFlags.MoveFiles | CopyActionFlags.Purge)] [DataRow(CopyActionFlags.MoveFilesAndDirectories)] [DataRow(CopyActionFlags.MoveFilesAndDirectories | CopyActionFlags.Purge)] - [DataTestMethod] public async Task ValidateRoboMover(CopyActionFlags copyOptions) { GetMoveCommands( @@ -306,7 +320,7 @@ public async Task ValidateRoboMover(CopyActionFlags copyOptions) DefaultLoggingAction, out _, out var rm); Test_Setup.ClearOutTestDestination(); - await PrepMoveFiles(); + await TestPrep.PrepMoveFiles(TestContext.CancellationToken); string subfolderpath = @"SubFolder_1\SubFolder_1.1\SubFolder_1.2"; FilePair[] SourceFiles = new FilePair[] { @@ -359,21 +373,5 @@ public async Task ValidateRoboMover(CopyActionFlags copyOptions) Assert.AreEqual(moveDirectories, SourceFiles[3].Parent.IsExtra(), moveDirectories ? "Directory was not moved" : "Directory was moved unexpectedly."); } - - [DataTestMethod] - public async Task CreateFilesToPurge() - { - await PrepMoveFiles(); - RoboCommand prep = new RoboCommand(); - prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_1"); - prep.CopyOptions.Destination = Path.Combine(Test_Setup.TestDestination, "SubFolder_3"); - prep.CopyOptions.ApplyActionFlags(CopyActionFlags.CopySubdirectoriesIncludingEmpty); - Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder1", "EmptyFolder2")); - await prep.Start(); - prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_2"); - prep.CopyOptions.Destination = Path.Combine(prep.CopyOptions.Destination, "SubFolder_2a"); - await prep.Start(); - Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder3", "EmptyFolder4")); - } } } \ No newline at end of file diff --git a/RoboSharp.Extensions.UnitTests/RoboSharp.Extensions.UnitTests.csproj b/RoboSharp.Extensions.UnitTests/RoboSharp.Extensions.UnitTests.csproj index cff1afc0..c07b8c89 100644 --- a/RoboSharp.Extensions.UnitTests/RoboSharp.Extensions.UnitTests.csproj +++ b/RoboSharp.Extensions.UnitTests/RoboSharp.Extensions.UnitTests.csproj @@ -7,7 +7,7 @@ - + diff --git a/RoboSharp.Extensions.UnitTests/TestPrep.cs b/RoboSharp.Extensions.UnitTests/TestPrep.cs index 18cf41e3..d5d015b9 100644 --- a/RoboSharp.Extensions.UnitTests/TestPrep.cs +++ b/RoboSharp.Extensions.UnitTests/TestPrep.cs @@ -1,16 +1,21 @@ -using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RoboSharp; +using RoboSharp.Interfaces; +using RoboSharp.UnitTests; +using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using RoboSharp; -using RoboSharp.Interfaces; -using RoboSharp.UnitTests; using TestSetup = RoboSharp.UnitTests.Test_Setup; namespace RoboSharp.Extensions.Tests { + /// + /// Extensions Test Helpers + /// public static class TestPrep { public static string SourceDirPath => RoboSharp.UnitTests.Test_Setup.Source_Standard; @@ -55,23 +60,24 @@ public static RoboCommand GetRoboCommand(bool useLargerFileSet, CopyActionFlags } - public static Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns) - => RunTests(roboCommand, customCommand, CleanBetweenRuns, taskBetweenRuns: null); + public static Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns, CancellationToken token) + => RunTests(roboCommand, customCommand, CleanBetweenRuns, taskBetweenRuns: null, token); - public static Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns, Action actionBetweenRuns) - => RunTests(roboCommand, customCommand, CleanBetweenRuns, taskBetweenRuns: actionBetweenRuns is null ? null : () => Task.Run(actionBetweenRuns)); + public static Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns, Action actionBetweenRuns, CancellationToken token) + => RunTests(roboCommand, customCommand, CleanBetweenRuns, taskBetweenRuns: actionBetweenRuns is null ? null : (c) => Task.Run(actionBetweenRuns, token), token); - public static async Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns, Func taskBetweenRuns) + public static async Task RunTests(RoboCommand roboCommand, IRoboCommand customCommand, bool CleanBetweenRuns, Func taskBetweenRuns, CancellationToken token) { var results = new List(); - await BetweenRuns(); - results.Add(await TestSetup.RunTest(roboCommand)); - if (!roboCommand.LoggingOptions.ListOnly) await BetweenRuns(); - + await BetweenRuns(token); + results.Add(await TestSetup.RunTest(roboCommand, token)); + if (!roboCommand.LoggingOptions.ListOnly) await BetweenRuns(token); + + token.ThrowIfCancellationRequested(); customCommand.OnError += CachedRoboCommand_OnError; customCommand.OnCommandError += CachedRoboCommand_OnCommandError; - - results.Add(await TestSetup.RunTest(customCommand)); + + results.Add(await TestSetup.RunTest(customCommand, token)); customCommand.OnError -= CachedRoboCommand_OnError; customCommand.OnCommandError -= CachedRoboCommand_OnCommandError; @@ -79,11 +85,12 @@ public static async Task RunTests(RoboCommand roboComman if (CleanBetweenRuns) TestSetup.ClearOutTestDestination(); return results.ToArray(); - async Task BetweenRuns() + async ValueTask BetweenRuns(CancellationToken token) { + token.ThrowIfCancellationRequested(); if (CleanBetweenRuns) TestSetup.ClearOutTestDestination(); if (taskBetweenRuns is not null) - await taskBetweenRuns(); + await taskBetweenRuns(token); } } private static void CachedRoboCommand_OnCommandError(IRoboCommand sender, CommandErrorEventArgs e) => Console.WriteLine(e.Exception); @@ -215,6 +222,45 @@ public static void CleanAppData() Directory.CreateDirectory(AppDataFolder); } } - + + + public static string GetMoveSource() + { + string original = TestPrep.SourceDirPath; + return Path.Combine(original.Replace(Path.GetFileName(original), ""), "MoveSource"); + } + + public static async Task PrepMoveFiles(CancellationToken token) + { + token.ThrowIfCancellationRequested(); + var rc = TestPrep.GetRoboCommand(false, CopyActionFlags.CopySubdirectoriesIncludingEmpty, SelectionFlags.Default, LoggingFlags.RoboSharpDefault | LoggingFlags.NoJobHeader); + rc.CopyOptions.Destination = GetMoveSource(); + Directory.CreateDirectory(rc.CopyOptions.Destination); + token.Register(() => rc.Stop()); + await rc.Start(); + var results = rc.GetResults(); + if (results.RoboCopyErrors.Length > 0) + throw new Exception( + "Prep Failed \n" + + string.Concat(args: results.RoboCopyErrors.Select(e => "\n RoboCommandError :\t" + e.GetType() + "\t" + e.ErrorDescription + "\t:\t" + e.ErrorPath).ToArray()) + + "\n" + ); + } + + public static async Task CreateFilesToPurge(CancellationToken token) + { + await PrepMoveFiles(token); + token.ThrowIfCancellationRequested(); + RoboCommand prep = new RoboCommand(); + prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_1"); + prep.CopyOptions.Destination = Path.Combine(Test_Setup.TestDestination, "SubFolder_3"); + prep.CopyOptions.ApplyActionFlags(CopyActionFlags.CopySubdirectoriesIncludingEmpty); + Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder1", "EmptyFolder2")); + await prep.Start(); + prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_2"); + prep.CopyOptions.Destination = Path.Combine(prep.CopyOptions.Destination, "SubFolder_2a"); + await prep.Start(); + Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder3", "EmptyFolder4")); + } } } diff --git a/RoboSharp.Extensions.UnitTests/Windows/CopyFileExTests.cs b/RoboSharp.Extensions.UnitTests/Windows/CopyFileExTests.cs index c8ab0eb1..1e125beb 100644 --- a/RoboSharp.Extensions.UnitTests/Windows/CopyFileExTests.cs +++ b/RoboSharp.Extensions.UnitTests/Windows/CopyFileExTests.cs @@ -6,7 +6,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using static RoboSharp.Extensions.Tests.AssertExtensions; #pragma warning disable CA1416 // Validate platform compatibility @@ -49,7 +48,7 @@ public async Task IFileCopierFactoryTests_CopyFileEx() await IFileCopierTests.RunTests(factory); } else - await Assert.ThrowsExceptionAsync(() => IFileCopierTests.RunTests(factory)); + await Assert.ThrowsAsync(() => IFileCopierTests.RunTests(factory)); } [TestMethod] @@ -97,7 +96,7 @@ public void CopyFileEx_CopyFile() // Source Missing Test if (File.Exists(sourceFile)) File.Delete(sourceFile); - Assert.ThrowsException(() => CopyFileEx.CopyFile(sourceFile, destFile, CopyFileExOptions.FAIL_IF_EXISTS)); + Assert.Throws(() => CopyFileEx.CopyFile(sourceFile, destFile, CopyFileExOptions.FAIL_IF_EXISTS)); // Prep for Fail_If_Exists Test File.WriteAllText(sourceFile, "Test Contents"); @@ -106,7 +105,7 @@ public void CopyFileEx_CopyFile() Assert.IsTrue(File.Exists(destFile)); // Fail_If_Exists -- Overwrite - Assert.ThrowsException(() => CopyFileEx.CopyFile(sourceFile, destFile, CopyFileExOptions.FAIL_IF_EXISTS), "\nCopy Operation Succeeded when CopyFileExOptions.FAIL_IF_EXISTS was set"); + Assert.Throws(() => CopyFileEx.CopyFile(sourceFile, destFile, CopyFileExOptions.FAIL_IF_EXISTS), "\nCopy Operation Succeeded when CopyFileExOptions.FAIL_IF_EXISTS was set"); Assert.IsTrue(CopyFileEx.CopyFile(sourceFile, destFile, CopyFileExOptions.NONE), "\n Copy Operation Failed when CopyFileExOptions.NONE was set"); // Cancellation @@ -117,7 +116,7 @@ public void CopyFileEx_CopyFile() return CopyProgressCallbackResult.CANCEL; }); Assert.IsFalse(callbackHit); - Assert.ThrowsException(() => CopyFileEx.CopyFile(sourceFile, destFile, default, cancelCallback), "\nOperation was not cancelled"); + Assert.Throws(() => CopyFileEx.CopyFile(sourceFile, destFile, default, cancelCallback), "\nOperation was not cancelled"); Assert.IsTrue(callbackHit, "\nCallback was not hit"); Assert.AreEqual(1, callbackHitCount, "\nCallback count incorrect"); callbackHit = false; @@ -172,7 +171,7 @@ public async Task CopyFileEx_CopyFileAsync() // Source Missing Test Console.WriteLine(string.Format("Source: {0}\nDestination: {1}", sourceFile, destFile)); if (File.Exists(sourceFile)) File.Delete(sourceFile); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, CopyFileExOptions.NONE)); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, CopyFileExOptions.NONE)); // Prep for Fail_If_Exists Test File.WriteAllText(sourceFile, "Test Contents"); @@ -181,7 +180,7 @@ public async Task CopyFileEx_CopyFileAsync() Assert.IsTrue(File.Exists(destFile)); // Fail_If_Exists -- Overwrite - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, CopyFileExOptions.FAIL_IF_EXISTS), "\nCopy Operation Succeeded when CopyFileExOptions.FAIL_IF_EXISTS was set"); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, CopyFileExOptions.FAIL_IF_EXISTS), "\nCopy Operation Succeeded when CopyFileExOptions.FAIL_IF_EXISTS was set"); Assert.IsTrue(await CopyFileEx.CopyFileAsync(sourceFile, destFile, CopyFileExOptions.NONE), "\n Copy Operation Failed when CopyFileExOptions.NONE was set"); // Cancellation @@ -192,7 +191,7 @@ public async Task CopyFileEx_CopyFileAsync() return CopyProgressCallbackResult.CANCEL; }); Assert.IsFalse(callbackHit); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, CopyFileExOptions.NONE, cancelCallback), "\nOperation was not cancelled"); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, CopyFileExOptions.NONE, cancelCallback), "\nOperation was not cancelled"); Assert.IsTrue(callbackHit, "\nCallback was not hit"); Assert.AreEqual(1, callbackHitCount, "\nCallback count incorrect"); callbackHit = false; @@ -272,12 +271,12 @@ string GetDestination() progSize.ProgressChanged += progSizeHandler; string assertMessage = "\n Source File Missing Test"; - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile), assertMessage); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, false), assertMessage); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, true), assertMessage); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progFull, 100, true), assertMessage); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progPercent, 100, true), assertMessage); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progSize, 100, true), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, false), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, true), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progFull, 100, true), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progPercent, 100, true), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progSize, 100, true), assertMessage); Assert.IsFalse(progFullUpdated | progSizeUpdated | progPercentUpdated); IFileCopierTests.CreateDummyFile(sourceFile, 3 * 1024 * 1024); @@ -287,11 +286,11 @@ string GetDestination() // Prevent Overwrite assertMessage = "\n Overwrite Prevention Test"; - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile), assertMessage); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, false), assertMessage); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progFull, 100, false), assertMessage); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progPercent, 100, false), assertMessage); - await Assert.ThrowsExceptionAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progSize, 100, false), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, false), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progFull, 100, false), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progPercent, 100, false), assertMessage); + await Assert.ThrowsAsync(async () => await CopyFileEx.CopyFileAsync(sourceFile, destFile, progSize, 100, false), assertMessage); // Overwrite progPercentUpdated = false; @@ -315,11 +314,11 @@ string GetDestination() var cdToken = new CancellationTokenSource(); cdToken.Cancel(); File.Delete(destFile); - await AssertExtensions.AssertThrowsExceptionAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, cdToken.Token), assertMessage); - await AssertExtensions.AssertThrowsExceptionAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, false, cdToken.Token), assertMessage); - await AssertExtensions.AssertThrowsExceptionAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progFull, 50, false, cdToken.Token), assertMessage); - await AssertExtensions.AssertThrowsExceptionAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progPercent, 50, false, cdToken.Token), assertMessage); - await AssertExtensions.AssertThrowsExceptionAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progSize, 50, false, cdToken.Token), assertMessage); + await Assert.ThrowsAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, cdToken.Token), assertMessage); + await Assert.ThrowsAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, false, cdToken.Token), assertMessage); + await Assert.ThrowsAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progFull, 50, false, cdToken.Token), assertMessage); + await Assert.ThrowsAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progPercent, 50, false, cdToken.Token), assertMessage); + await Assert.ThrowsAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progSize, 50, false, cdToken.Token), assertMessage); Assert.IsFalse(File.Exists(destFile)); // Cancellation Mid-Write - These tests have potential to fail due to race condition with small file size when run on Appveyor (which completes copy operation before cancellation occurs) @@ -339,10 +338,10 @@ void Cancel(object o, T obj) } File.Delete(destFile); CopyProgressCallback midWriteCancelCallback = new CopyProgressCallback((a, b, c, d, e, f) => CopyProgressCallbackResult.CANCEL); - await AssertExtensions.AssertThrowsExceptionAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, CopyFileExOptions.NONE, midWriteCancelCallback, CancellationToken.None), assertMessage + 1); - await AssertExtensions.AssertThrowsExceptionAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progFull, 5, false, GetProgToken(progFull)), assertMessage + 2); - await AssertExtensions.AssertThrowsExceptionAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progPercent, 5, false, GetProgToken(progPercent)), assertMessage + 3); - await AssertExtensions.AssertThrowsExceptionAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progSize, 5, false, GetProgToken(progSize)), assertMessage + 4); + await Assert.ThrowsAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, CopyFileExOptions.NONE, midWriteCancelCallback, CancellationToken.None), assertMessage + 1); + await Assert.ThrowsAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progFull, 5, false, GetProgToken(progFull)), assertMessage + 2); + await Assert.ThrowsAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progPercent, 5, false, GetProgToken(progPercent)), assertMessage + 3); + await Assert.ThrowsAsync(() => CopyFileEx.CopyFileAsync(sourceFile, destFile, progSize, 5, false, GetProgToken(progSize)), assertMessage + 4); // These progress report assertions are to check that the operation STARTED but was cancelled prior to completion, causing deletion because Restartable mode was not used. Assert.IsFalse(File.Exists(destFile)); } diff --git a/RoboSharpUnitTesting/ProgressEstimatorTests.cs b/RoboSharpUnitTesting/ProgressEstimatorTests.cs index b87e0255..d1b05d77 100644 --- a/RoboSharpUnitTesting/ProgressEstimatorTests.cs +++ b/RoboSharpUnitTesting/ProgressEstimatorTests.cs @@ -25,6 +25,8 @@ public class ProgressEstimatorTests { public virtual bool ListOnlyMode => false; + public TestContext TestContext { get; set; } + //[TestMethod] public async Task SAMPLE_TEST_METHOD() { @@ -33,12 +35,12 @@ public async Task SAMPLE_TEST_METHOD() //Run the test and Evaluate the results and pass/Fail the test - RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); UnitTestResults.AssertTest(); } - [TestMethod] + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public async Task Test_NoCopyOptions() { // Create the Command @@ -46,46 +48,46 @@ public async Task Test_NoCopyOptions() //Run the test - First Test should just use default values generated from the GenerateCommand method! Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); //Evaluate the results and pass/Fail the test UnitTestResults.AssertTest(); } - [TestMethod] + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public async Task Test_ExcludedFiles() { // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(false, ListOnlyMode); cmd.SelectionOptions.ExcludedFiles.Add("4_Bytes.txt"); // 3 copies of this file exist Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); UnitTestResults.AssertTest();//Evaluate the results and pass/Fail the test } - [TestMethod] + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public async Task Test_MinFileSize() { // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(true, ListOnlyMode); cmd.SelectionOptions.MinFileSize = 1500; Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); UnitTestResults.AssertTest();//Evaluate the results and pass/Fail the test } - [TestMethod] + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public async Task Test_MaxFileSize() { // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(true, ListOnlyMode); cmd.SelectionOptions.MaxFileSize = 1500; Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); UnitTestResults.AssertTest();//Evaluate the results and pass/Fail the test } - [TestMethod] + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public async Task Test_FileInUse() { if (Test_Setup.IsRunningOnAppVeyor()) return; @@ -112,7 +114,7 @@ public async Task Test_FileInUse() Console.WriteLine("Creating and locking file: " + fPath); var f = File.Open(fPath, FileMode.Create); Console.WriteLine("Running Test"); - UnitTestResults = await Test_Setup.RunTest(cmd); + UnitTestResults = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); Console.WriteLine("Test Complete"); Console.WriteLine("Releasing File: " + fPath); f.Close(); @@ -128,7 +130,7 @@ public async Task Test_FileInUse() UnitTestResults.AssertTest(); } - [TestMethod] + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public async Task Test_ExcludeLastAccessDate() { //Create the command and base values for the Expected Results @@ -144,7 +146,7 @@ public async Task Test_ExcludeLastAccessDate() cmd.SelectionOptions.MaxLastAccessDate = "19900101"; Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); //Evaluate the results and pass/Fail the test UnitTestResults.AssertTest(); @@ -159,12 +161,13 @@ public async Task Test_ExcludeLastAccessDate() [DataRow(1)] [DataRow(8)] [TestMethod] + [Timeout(2000, CooperativeCancellation = true)] public async Task TestMultiThread(int threads) { RoboCommand cmd = Test_Setup.GenerateCommand(false, ListOnlyMode); cmd.CopyOptions.MultiThreadedCopiesCount = threads; Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); // Ignore Directory Statistics during a multithread test, as they are not reported by robocopy List Errors = new List(); @@ -196,27 +199,23 @@ public async Task TestMultiThread(int threads) */ //INCLUDE - [TestMethod] public Task Test_IncludeAttribReadOnly() => Test_Attributes(FileAttributes.ReadOnly, true); - [TestMethod] public Task Test_IncludeAttribArchive() => Test_Attributes(FileAttributes.Archive, true); - [TestMethod] public Task Test_IncludeAttribSystem() => Test_Attributes(FileAttributes.System, true); - [TestMethod] public Task Test_IncludeAttribHidden() => Test_Attributes(FileAttributes.Hidden, true); - //[TestMethod] public Task Test_IncludeAttribCompressed() => Test_Attributes(FileAttributes.Compressed, true); - [TestMethod] public Task Test_IncludeAttribNotContentIndexed() => Test_Attributes(FileAttributes.NotContentIndexed, true); - //[TestMethod] public Task Test_IncludeAttribEncrypted() => Test_Attributes(FileAttributes.Encrypted, true); - [TestMethod] public Task Test_IncludeAttribTemporary() => Test_Attributes(FileAttributes.Temporary, true); - [TestMethod] public Task Test_IncludeAttribOffline() => Test_Attributes(FileAttributes.Offline, true); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_IncludeAttribReadOnly() => Test_Attributes(FileAttributes.ReadOnly, true); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_IncludeAttribArchive() => Test_Attributes(FileAttributes.Archive, true); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_IncludeAttribSystem() => Test_Attributes(FileAttributes.System, true); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_IncludeAttribHidden() => Test_Attributes(FileAttributes.Hidden, true); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_IncludeAttribNotContentIndexed() => Test_Attributes(FileAttributes.NotContentIndexed, true); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_IncludeAttribTemporary() => Test_Attributes(FileAttributes.Temporary, true); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_IncludeAttribOffline() => Test_Attributes(FileAttributes.Offline, true); //EXCLUDE - [TestMethod] public Task Test_ExcludeAttribReadOnly() => Test_Attributes(FileAttributes.ReadOnly, false); - [TestMethod] public Task Test_ExcludeAttribArchive() => Test_Attributes(FileAttributes.Archive, false); - [TestMethod] public Task Test_ExcludeAttribSystem() => Test_Attributes(FileAttributes.System, false); - [TestMethod] public Task Test_ExcludeAttribHidden() => Test_Attributes(FileAttributes.Hidden, false); - //[TestMethod] public Task Test_ExcludeAttribCompressed() => Test_Attributes(FileAttributes.Compressed, false); - [TestMethod] public Task Test_ExcludeAttribNotContentIndexed() => Test_Attributes(FileAttributes.NotContentIndexed, false); - //[TestMethod] public Task Test_ExcludeAttribEncrypted() => Test_Attributes(FileAttributes.Encrypted, false); - [TestMethod] public Task Test_ExcludeAttribTemporary() => Test_Attributes(FileAttributes.Temporary, false); - [TestMethod] public Task Test_ExcludeAttribOffline() => Test_Attributes(FileAttributes.Offline, false); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_ExcludeAttribReadOnly() => Test_Attributes(FileAttributes.ReadOnly, false); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_ExcludeAttribArchive() => Test_Attributes(FileAttributes.Archive, false); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_ExcludeAttribSystem() => Test_Attributes(FileAttributes.System, false); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_ExcludeAttribHidden() => Test_Attributes(FileAttributes.Hidden, false); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_ExcludeAttribNotContentIndexed() => Test_Attributes(FileAttributes.NotContentIndexed, false); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_ExcludeAttribTemporary() => Test_Attributes(FileAttributes.Temporary, false); + [TestMethod, Timeout(2000, CooperativeCancellation = true)] public Task Test_ExcludeAttribOffline() => Test_Attributes(FileAttributes.Offline, false); #pragma warning disable IDE0059 // Unnecessary assignment of a value @@ -224,6 +223,8 @@ public async Task TestMultiThread(int threads) /// TRUE if setting to INCLUDE, False to EXCLUDE private async Task Test_Attributes(FileAttributes attributes, bool Include) { + TestContext.CancellationToken.ThrowIfCancellationRequested(); + // Create the Command RoboCommand cmd = Test_Setup.GenerateCommand(false, ListOnlyMode); @@ -261,8 +262,9 @@ private async Task Test_Attributes(FileAttributes attributes, bool Include) expectedFileCounts.SetValues(12, 11, 0, 0, 0, 1); } + TestContext.CancellationToken.ThrowIfCancellationRequested(); Test_Setup.ClearOutTestDestination(); - RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd); + RoboSharpTestResults UnitTestResults = await Test_Setup.RunTest(cmd, TestContext.CancellationToken); //Revert all modified files to their normal state File.SetAttributes(filePath, FileAttributes.Normal); //Source File diff --git a/RoboSharpUnitTesting/RoboCommandParserTests.cs b/RoboSharpUnitTesting/RoboCommandParserTests.cs index 09294b0c..ab509aa8 100644 --- a/RoboSharpUnitTesting/RoboCommandParserTests.cs +++ b/RoboSharpUnitTesting/RoboCommandParserTests.cs @@ -430,7 +430,7 @@ public void Test_ParseSourceAndDestination(string source, string dest) public void Test_ParseSourceAndDestinationException(string source, string destination, bool shouldThrow = true, bool wrap = true) { if (shouldThrow) - Assert.ThrowsException(runTest); + Assert.Throws(runTest); else runTest(); diff --git a/RoboSharpUnitTesting/RoboSharpUnitTesting.csproj b/RoboSharpUnitTesting/RoboSharpUnitTesting.csproj index 10845370..3fa322a1 100644 --- a/RoboSharpUnitTesting/RoboSharpUnitTesting.csproj +++ b/RoboSharpUnitTesting/RoboSharpUnitTesting.csproj @@ -23,7 +23,7 @@ - + diff --git a/RoboSharpUnitTesting/SelectionOptionsTests.cs b/RoboSharpUnitTesting/SelectionOptionsTests.cs index 66be79e0..3b82cd6d 100644 --- a/RoboSharpUnitTesting/SelectionOptionsTests.cs +++ b/RoboSharpUnitTesting/SelectionOptionsTests.cs @@ -71,7 +71,7 @@ public void Test_ConvertFileAttrToString(string output, FileAttributes? input) [TestMethod] public void Test_ConvertFileAttrToString_InvalidString() { - Assert.ThrowsException(() => SelectionOptions.ConvertFileAttrStringToEnum("Q")); + Assert.Throws(() => SelectionOptions.ConvertFileAttrStringToEnum("Q")); } [DataRow("", null)] diff --git a/RoboSharpUnitTesting/Test_Setup.cs b/RoboSharpUnitTesting/Test_Setup.cs index 78923d1b..3cab030c 100644 --- a/RoboSharpUnitTesting/Test_Setup.cs +++ b/RoboSharpUnitTesting/Test_Setup.cs @@ -59,10 +59,12 @@ public static RoboCommand GenerateCommand(bool UseLargerFileSet, bool ListOnlyMo return cmd; } - public static async Task RunTest(IRoboCommand cmd) + public static async Task RunTest(IRoboCommand cmd, CancellationToken token) { IProgressEstimator prog = null; cmd.OnProgressEstimatorCreated += (o, e) => prog = e.ResultsEstimate; + token.ThrowIfCancellationRequested(); + token.Register(() => cmd.Stop()); var results = await cmd.StartAsync(); return new RoboSharpTestResults(results, prog); } @@ -125,7 +127,6 @@ public static void SetValues(this Statistic stat, int total, int copied, int fai stat.Reset(); stat.Add(total, copied, extras, failed, mismatch, skipped); } - } } From e611a231e25cd493dfe18200b9a95e5176c505b3 Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:30:12 -0400 Subject: [PATCH 08/16] Move IFilePairExtensions and IDirectoryPairExtensions --- RoboSharp.Extensions.UnitTests/BatchCommandTests.cs | 1 - RoboSharp.Extensions.UnitTests/DirectoryPairTests.cs | 1 - RoboSharp.Extensions.UnitTests/RoboMoverTests.cs | 1 - .../{Helpers => }/IDirectoryPairExtensions.cs | 11 ++++------- .../{Helpers => }/IFilePairExtensions.cs | 7 ++----- RoboSharp.Extensions/Options/CopyExtensions.cs | 1 - RoboSharp.Extensions/RoboCommandPortable.cs | 6 +++--- 7 files changed, 9 insertions(+), 19 deletions(-) rename RoboSharp.Extensions/{Helpers => }/IDirectoryPairExtensions.cs (98%) rename RoboSharp.Extensions/{Helpers => }/IFilePairExtensions.cs (97%) diff --git a/RoboSharp.Extensions.UnitTests/BatchCommandTests.cs b/RoboSharp.Extensions.UnitTests/BatchCommandTests.cs index 1af875a7..6ed283ba 100644 --- a/RoboSharp.Extensions.UnitTests/BatchCommandTests.cs +++ b/RoboSharp.Extensions.UnitTests/BatchCommandTests.cs @@ -1,6 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using RoboSharp; -using RoboSharp.Extensions.Helpers; using RoboSharp.Interfaces; using RoboSharp.UnitTests; using System; diff --git a/RoboSharp.Extensions.UnitTests/DirectoryPairTests.cs b/RoboSharp.Extensions.UnitTests/DirectoryPairTests.cs index b350ac5a..ba5c1dc3 100644 --- a/RoboSharp.Extensions.UnitTests/DirectoryPairTests.cs +++ b/RoboSharp.Extensions.UnitTests/DirectoryPairTests.cs @@ -1,6 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using RoboSharp; -using RoboSharp.Extensions.Helpers; using RoboSharp.Interfaces; using RoboSharp.UnitTests; using System; diff --git a/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs b/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs index 0153f45a..80d4059f 100644 --- a/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs +++ b/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs @@ -1,6 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using RoboSharp; -using RoboSharp.Extensions.Helpers; using RoboSharp.Interfaces; using RoboSharp.UnitTests; using System; diff --git a/RoboSharp.Extensions/Helpers/IDirectoryPairExtensions.cs b/RoboSharp.Extensions/IDirectoryPairExtensions.cs similarity index 98% rename from RoboSharp.Extensions/Helpers/IDirectoryPairExtensions.cs rename to RoboSharp.Extensions/IDirectoryPairExtensions.cs index 700853b7..82fac5b0 100644 --- a/RoboSharp.Extensions/Helpers/IDirectoryPairExtensions.cs +++ b/RoboSharp.Extensions/IDirectoryPairExtensions.cs @@ -1,15 +1,12 @@ -using RoboSharp.Extensions.Options; +using RoboSharp.Extensions.Helpers; +using RoboSharp.Extensions.Options; using RoboSharp.Interfaces; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -namespace RoboSharp.Extensions.Helpers +namespace RoboSharp.Extensions { /// @@ -88,7 +85,7 @@ public static bool TrySetSizeAndPath(this IProcessedDirectoryPair pair, bool pri /// true if the directory should be processed further for COPYING, otherwise false. ///
Note: purging/mirroing is ignored for this evaluation. /// - public static bool EvaluateDirectoryPair(this IProcessedDirectoryPair pair, IRoboCommand command, IEnumerable directoryExclusionRegex, bool getFileCount = false) + public static bool EvaluateCommandOptions(this IProcessedDirectoryPair pair, IRoboCommand command, IEnumerable directoryExclusionRegex, bool getFileCount = false) { var info = pair.ProcessedFileInfo ??= new ProcessedFileInfo(); diff --git a/RoboSharp.Extensions/Helpers/IFilePairExtensions.cs b/RoboSharp.Extensions/IFilePairExtensions.cs similarity index 97% rename from RoboSharp.Extensions/Helpers/IFilePairExtensions.cs rename to RoboSharp.Extensions/IFilePairExtensions.cs index 30b5d07a..73d2b45b 100644 --- a/RoboSharp.Extensions/Helpers/IFilePairExtensions.cs +++ b/RoboSharp.Extensions/IFilePairExtensions.cs @@ -3,13 +3,10 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -namespace RoboSharp.Extensions.Helpers +namespace RoboSharp.Extensions { /// /// Extension Methods for the interface @@ -24,7 +21,7 @@ public static class IFilePairExtensions /// /// /// - public static bool ProcessFilePairAgainstCommandOptions(this IFileCopier pair, IRoboCommand command, IEnumerable copyOptions_FileNameNameInclusions, IEnumerable SelectionOptions_FileNameNameExclusions) + public static bool EvaluateCommandOptions(this IFileCopier pair, IRoboCommand command, IEnumerable copyOptions_FileNameNameInclusions, IEnumerable SelectionOptions_FileNameNameExclusions) { var sOptions = command.SelectionOptions; pair.ShouldCopy = false; diff --git a/RoboSharp.Extensions/Options/CopyExtensions.cs b/RoboSharp.Extensions/Options/CopyExtensions.cs index 4d4f772b..285803f5 100644 --- a/RoboSharp.Extensions/Options/CopyExtensions.cs +++ b/RoboSharp.Extensions/Options/CopyExtensions.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -using RoboSharp.Extensions.Helpers; namespace RoboSharp.Extensions.Options { diff --git a/RoboSharp.Extensions/RoboCommandPortable.cs b/RoboSharp.Extensions/RoboCommandPortable.cs index ddeea303..943c666b 100644 --- a/RoboSharp.Extensions/RoboCommandPortable.cs +++ b/RoboSharp.Extensions/RoboCommandPortable.cs @@ -219,8 +219,8 @@ void PostRunListOnlyAction() private DirectoryRegex[] GetDirectoryRegexes() => directoryRegexes ??= SelectionOptions.GetExcludedDirectoryRegex(); private DirectoryRegex[]? directoryRegexes; - private void EvaluateFilePair(IFileCopier pair) => pair.ProcessFilePairAgainstCommandOptions(this, GetFileFilterRegex(), GetFileExclusionRegex()); - private void EvaluateDirPair(DirectoryPair pair) => pair.EvaluateDirectoryPair(this, GetDirectoryRegexes()); + private void EvaluateFilePair(IFileCopier pair) => pair.EvaluateCommandOptions(this, GetFileFilterRegex(), GetFileExclusionRegex()); + private void EvaluateDirPair(DirectoryPair pair) => pair.EvaluateCommandOptions(this, GetDirectoryRegexes()); private void RaiseProgressUpdated(object? sender, CopyProgressEventArgs e) => OnCopyProgressChanged?.Invoke(this, e); @@ -383,7 +383,7 @@ private async Task RunAsync(CancellationToken cancellationToken) } } - // ── 2a. Source files ────────────────────────────────────────────────── + // ── Process Source files for copy/move ────────────────────────────────────────────────── if (dirPair.Source.Exists) { if (includeEmpty) From 4fa30b1399a38f6f868cc774fcd1b619c9f0e1f5 Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:53:08 -0400 Subject: [PATCH 09/16] Resolve unit RoboCommandPortable tests (except Purge) --- .../RoboCommandPortableTests.cs | 33 ++++--- RoboSharp.Extensions.UnitTests/TestPrep.cs | 3 + .../Helpers/ResultsBuilder.cs | 7 +- RoboSharp.Extensions/IFilePairExtensions.cs | 79 ++++++++++------ .../Options/CopyExtensions.cs | 3 +- RoboSharp.Extensions/RoboCommandPortable.cs | 91 ++++++++++--------- 6 files changed, 129 insertions(+), 87 deletions(-) diff --git a/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs b/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs index f0304c12..b6abd9e0 100644 --- a/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs +++ b/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs @@ -62,6 +62,12 @@ private static RoboCommandPortable GetCommand(RoboCommand rc, IFileCopierFactory }; } + [TestCleanup] + public void TestCleanup() + { + TestPrep.CleanDestination(); + } + [TestMethod] [Timeout(1000, CooperativeCancellation = true)] @@ -211,7 +217,7 @@ static async Task CreateFile(CancellationToken token) } [TestMethod] - [Timeout(5000, CooperativeCancellation = true)] + //[Timeout(5000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -228,10 +234,9 @@ static async Task CreateFile(CancellationToken token) { await TestPrep.PrepMoveFiles(token); Directory.CreateDirectory(TestPrep.DestDirPath); - string fn = "1024_Bytes.txt"; - string dest = Path.Combine(TestPrep.DestDirPath, fn); - if (!File.Exists(dest)) - File.Copy(Path.Combine(TestPrep.SourceDirPath, fn), dest); + string dest = Path.Combine(TestPrep.DestDirPath, Path.GetRandomFileName()); + File.WriteAllText(dest, "!!!!This is an extra File!!!!"); + Assert.IsTrue(File.Exists(dest)); } } @@ -267,10 +272,12 @@ static async Task CreateFile(CancellationToken token) public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; - GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); + GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var implementation); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.Depth = depth; - await RunPurge(cmd, mover, TestContext.CancellationToken); + cmd.LoggingOptions.ReportExtraFiles = true; + cmd.LoggingOptions.VerboseOutput = true; + await RunPurge(cmd, implementation, TestContext.CancellationToken); } [TestMethod] @@ -283,10 +290,10 @@ public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, L [DataRow(false, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) { - GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); + GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var implementation); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedFiles.Add("*0*_Bytes.txt"); - await RunPurge(cmd, mover, TestContext.CancellationToken); + await RunPurge(cmd, implementation, TestContext.CancellationToken); } [TestMethod] @@ -299,12 +306,12 @@ public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) { - GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); + GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var implementation); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder1"); // Top level empty cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder4"); // Bottom level empty cmd.SelectionOptions.ExcludedDirectories.Add("SubFolder_2a"); // folder with contents - await RunPurge(cmd, mover, TestContext.CancellationToken); + await RunPurge(cmd, implementation, TestContext.CancellationToken); } [TestMethod] @@ -330,10 +337,10 @@ public async Task Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, Logg await RunPurge(cmd, mover, TestContext.CancellationToken); } - private static async Task RunPurge(RoboCommand cmd, RoboCommandPortable mover, CancellationToken token) + private static async Task RunPurge(RoboCommand cmd, RoboCommandPortable implementation, CancellationToken token) { //if (Test_Setup.IsRunningOnAppVeyor()) return; - var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, TestPrep.CreateFilesToPurge, token); + var results = await TestPrep.RunTests(cmd, implementation, !cmd.LoggingOptions.ListOnly, TestPrep.CreateFilesToPurge, token); TestPrep.CompareTestResults(results[0], results[1], cmd.LoggingOptions.ListOnly); } diff --git a/RoboSharp.Extensions.UnitTests/TestPrep.cs b/RoboSharp.Extensions.UnitTests/TestPrep.cs index d5d015b9..728a4fe6 100644 --- a/RoboSharp.Extensions.UnitTests/TestPrep.cs +++ b/RoboSharp.Extensions.UnitTests/TestPrep.cs @@ -252,11 +252,14 @@ public static async Task CreateFilesToPurge(CancellationToken token) await PrepMoveFiles(token); token.ThrowIfCancellationRequested(); RoboCommand prep = new RoboCommand(); + token.Register(() => prep.Stop()); + prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_1"); prep.CopyOptions.Destination = Path.Combine(Test_Setup.TestDestination, "SubFolder_3"); prep.CopyOptions.ApplyActionFlags(CopyActionFlags.CopySubdirectoriesIncludingEmpty); Directory.CreateDirectory(Path.Combine(prep.CopyOptions.Destination, "EmptyFolder1", "EmptyFolder2")); await prep.Start(); + prep.CopyOptions.Source = Path.Combine(Test_Setup.Source_Standard, "SubFolder_2"); prep.CopyOptions.Destination = Path.Combine(prep.CopyOptions.Destination, "SubFolder_2a"); await prep.Start(); diff --git a/RoboSharp.Extensions/Helpers/ResultsBuilder.cs b/RoboSharp.Extensions/Helpers/ResultsBuilder.cs index 1fea079c..6581c4b2 100644 --- a/RoboSharp.Extensions/Helpers/ResultsBuilder.cs +++ b/RoboSharp.Extensions/Helpers/ResultsBuilder.cs @@ -171,7 +171,8 @@ public virtual void AddFileCopied(ProcessedFileInfo file) public virtual void AddFileExtra(ProcessedFileInfo file) { ProgressEstimator.AddFileExtra(file); - LogFileInfo(file); + if (Command.LoggingOptions.VerboseOutput || Command.LoggingOptions.ReportExtraFiles) + LogFileInfo(file); } /// @@ -219,7 +220,7 @@ public virtual void AddFilePurged(ProcessedFileInfo file) public virtual void AddFileSkipped(ProcessedFileInfo file) { ProgressEstimator.AddFileSkipped(file); - if (Command.LoggingOptions.ReportExtraFiles) + if (Command.LoggingOptions.VerboseOutput) LogFileInfo(file); } @@ -262,7 +263,9 @@ public void AddFirstDir(IProcessedDirectoryPair topLevelDirectory) { var info = topLevelDirectory.ProcessedFileInfo; if (topLevelDirectory.Destination.Exists) + { ProgressEstimator.AddDirSkipped(info); + } else { info.SetDirectoryClass(ProcessedDirectoryFlag.NewDir, Command.Configuration); diff --git a/RoboSharp.Extensions/IFilePairExtensions.cs b/RoboSharp.Extensions/IFilePairExtensions.cs index 73d2b45b..f131417c 100644 --- a/RoboSharp.Extensions/IFilePairExtensions.cs +++ b/RoboSharp.Extensions/IFilePairExtensions.cs @@ -13,6 +13,27 @@ namespace RoboSharp.Extensions /// public static class IFilePairExtensions { + /// + /// A return result for + /// + public enum EvaluationResult + { + /// + /// File was Excluded for one of the various reasons. + /// + Excluded, + + /// + /// File was skipped because it does not match the + /// + SkippedByFilter, + + /// + /// File selected for processing. + /// + Included + } + /// /// Evaluate a against the options to determine if it should be copied or not. /// @@ -21,101 +42,101 @@ public static class IFilePairExtensions /// /// /// - public static bool EvaluateCommandOptions(this IFileCopier pair, IRoboCommand command, IEnumerable copyOptions_FileNameNameInclusions, IEnumerable SelectionOptions_FileNameNameExclusions) + public static EvaluationResult EvaluateCommandOptions(this IFileCopier pair, IRoboCommand command, IEnumerable copyOptions_FileNameNameInclusions, IEnumerable SelectionOptions_FileNameNameExclusions) { var sOptions = command.SelectionOptions; pair.ShouldCopy = false; - + pair.ShouldPurge = false; + // Extra if (pair.IsExtra()) { pair.ProcessedFileInfo = new ProcessedFileInfo(pair.Destination, command, ProcessedFileFlag.ExtraFile); _ = command.ShouldPurge(pair); // process for purging - return false; + return EvaluationResult.Excluded; } - pair.ShouldPurge = false; ProcessedFileInfo pInfo = pair.ProcessedFileInfo ??= new ProcessedFileInfo(pair.Source, command, ProcessedFileFlag.None); + // evaluate Names + if (!Options.CopyExtensions.ShouldIncludeFileName(command.CopyOptions, pair.Source, copyOptions_FileNameNameInclusions)) + { + pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); + return EvaluationResult.SkippedByFilter; + } + + if (Options.SelectionExtensions.ShouldExcludeFileName(command.SelectionOptions, pair.Source, SelectionOptions_FileNameNameExclusions)) + { + pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); + return EvaluationResult.Excluded; + } + // lonely files if (sOptions.ShouldExcludeLonely(pair)) { pInfo.SetFileClass(ProcessedFileFlag.NewFile, command.Configuration); - return false; + return EvaluationResult.Excluded; } // file age if (sOptions.ShouldExcludeMaxFileAge(pair)) { pInfo.SetFileClass(ProcessedFileFlag.MaxAgeSizeExclusion, command.Configuration); - return false; + return EvaluationResult.Excluded; } if (sOptions.ShouldExcludeMinFileAge(pair)) { pInfo.SetFileClass(ProcessedFileFlag.MinAgeSizeExclusion, command.Configuration); - return false; + return EvaluationResult.Excluded; } // file size if (sOptions.ShouldExcludeMaxFileSize(pair)) { pInfo.SetFileClass(ProcessedFileFlag.MaxFileSizeExclusion, command.Configuration); - return false; + return EvaluationResult.Excluded; } if (sOptions.ShouldExcludeMinFileSize(pair)) { pInfo.SetFileClass(ProcessedFileFlag.MinFileSizeExclusion, command.Configuration); - return false; + return EvaluationResult.Excluded; } // older / newer if (sOptions.ShouldExcludeNewer(pair)) { pInfo.SetFileClass(ProcessedFileFlag.NewerFile, command.Configuration); - return false; + return EvaluationResult.Excluded; } if (sOptions.ShouldExcludeOlder(pair)) { pInfo.SetFileClass(ProcessedFileFlag.OlderFile, command.Configuration); - return false; + return EvaluationResult.Excluded; } // access date if (sOptions.ShouldExcludeMaxLastAccessDate(pair)) { pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); - return false; + return EvaluationResult.Excluded; } if (sOptions.ShouldExcludeMinLastAccessDate(pair)) { pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); - return false; + return EvaluationResult.Excluded; } if (sOptions.ShouldIncludeAttributes(pair) == false) { pInfo.SetFileClass(ProcessedFileFlag.AttribExclusion, command.Configuration); - return false; - } - - // potentially expensive regex tests - if (!Options.CopyExtensions.ShouldIncludeFileName(command.CopyOptions, pair.Source, copyOptions_FileNameNameInclusions)) - { - pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); - return false; - } - - if (Options.SelectionExtensions.ShouldExcludeFileName(command.SelectionOptions, pair.Source, SelectionOptions_FileNameNameExclusions)) - { - pInfo.SetFileClass(ProcessedFileFlag.FileExclusion, command.Configuration); - return false; + return EvaluationResult.Excluded; } if (pair.IsSameDate()) { pInfo.SetFileClass(ProcessedFileFlag.SameFile, command.Configuration); pair.ShouldCopy = command.SelectionOptions.IncludeSame; - return pair.ShouldCopy; + return pair.ShouldCopy ? EvaluationResult.Included : EvaluationResult.Excluded; } pair.ShouldCopy = true; @@ -125,7 +146,7 @@ public static bool EvaluateCommandOptions(this IFileCopier pair, IRoboCommand co : pair.IsSameDate() ? ProcessedFileFlag.SameFile : ProcessedFileFlag.TweakedInclusion; pInfo.SetFileClass(flag, command.Configuration); - return true; + return EvaluationResult.Included; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/RoboSharp.Extensions/Options/CopyExtensions.cs b/RoboSharp.Extensions/Options/CopyExtensions.cs index 285803f5..a649bf47 100644 --- a/RoboSharp.Extensions/Options/CopyExtensions.cs +++ b/RoboSharp.Extensions/Options/CopyExtensions.cs @@ -163,8 +163,7 @@ public static bool HasDefaultFileFilter(this CopyOptions options) /// public static bool ShouldIncludeFileName(this CopyOptions options, string fileName, IEnumerable fileFilterRegex = null) { - - if (fileFilterRegex is null) fileFilterRegex = options.GetFileFilterRegex(); + fileFilterRegex ??= options.GetFileFilterRegex(); if (fileFilterRegex.None()) return true; return fileFilterRegex.Any(r => r.IsMatch(fileName)); } diff --git a/RoboSharp.Extensions/RoboCommandPortable.cs b/RoboSharp.Extensions/RoboCommandPortable.cs index 943c666b..b74748de 100644 --- a/RoboSharp.Extensions/RoboCommandPortable.cs +++ b/RoboSharp.Extensions/RoboCommandPortable.cs @@ -74,7 +74,7 @@ public RoboCommandPortable(IFileCopierFactory fileCopierFactory, IAuthenticator? public event RoboCommand.CommandCompletedHandler? OnCommandCompleted; public event RoboCommand.CopyProgressHandler? OnCopyProgressChanged; public event RoboCommand.ProgressUpdaterCreatedHandler? OnProgressEstimatorCreated; - public event UnhandledExceptionEventHandler? TaskFaulted; + public event UnhandledExceptionEventHandler? TaskFaulted { add { } remove { } } public event PropertyChangedEventHandler? PropertyChanged; @@ -219,7 +219,7 @@ void PostRunListOnlyAction() private DirectoryRegex[] GetDirectoryRegexes() => directoryRegexes ??= SelectionOptions.GetExcludedDirectoryRegex(); private DirectoryRegex[]? directoryRegexes; - private void EvaluateFilePair(IFileCopier pair) => pair.EvaluateCommandOptions(this, GetFileFilterRegex(), GetFileExclusionRegex()); + private IFilePairExtensions.EvaluationResult EvaluateFilePair(IFileCopier pair) => pair.EvaluateCommandOptions(this, GetFileFilterRegex(), GetFileExclusionRegex()); private void EvaluateDirPair(DirectoryPair pair) => pair.EvaluateCommandOptions(this, GetDirectoryRegexes()); private void RaiseProgressUpdated(object? sender, CopyProgressEventArgs e) => OnCopyProgressChanged?.Invoke(this, e); @@ -286,7 +286,7 @@ private async Task RunAsync(CancellationToken cancellationToken) bool includeEmpty = this.CopyOptions.CopySubdirectoriesIncludingEmpty || CopyOptions.Mirror; bool recurse = this.CopyOptions.CopySubdirectories || this.CopyOptions.CopySubdirectoriesIncludingEmpty || CopyOptions.Mirror; - int maxDepth = !recurse ? 1 : CopyOptions.Depth == 0 ? int.MaxValue : CopyOptions.Depth; + int maxDepth = recurse ? (CopyOptions.Depth <= 0 ? int.MaxValue : CopyOptions.Depth) : 1; var rootPair = new DirectoryPair(this.CopyOptions.Source, this.CopyOptions.Destination); bool listOnly = LoggingOptions.ListOnly; bool touchFiles = CopyOptions.CreateDirectoryAndFileTree; @@ -303,14 +303,13 @@ private async Task RunAsync(CancellationToken cancellationToken) // Robocopy reports totals before starting transfers; we replicate that here // so ProgressEstimator can give accurate percentage estimates from the start. - await foreach (var dirPair in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) + await foreach (DirectoryPair dirPair in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) { // Tell the estimator a directory exists on the source side EvaluateDirPair(dirPair); progressReporter.AddDir(dirPair.ProcessedFileInfo); infoDict[dirPair.Source.FullName] = dirPair.ProcessedFileInfo; - await foreach (IFileCopier copier in CreateFileCopiers(dirPair, cancellationToken)) { dirPair.ProcessedFileInfo.Size++; @@ -319,6 +318,8 @@ private async Task RunAsync(CancellationToken cancellationToken) // ── Pass 2: process each directory ─────────────────────────────────────── await foreach (var dirPair in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) + //(DirectoryPair dirPair, int currentDepth) + await foreach (DirectoryPair dirPair in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -333,9 +334,17 @@ private async Task RunAsync(CancellationToken cancellationToken) } progressReporter.AddDir(dirPair.ProcessedFileInfo); - resultsBuilder.AddDir(dirPair.ProcessedFileInfo); + if (dirPair == rootPair) + { + resultsBuilder.AddFirstDir(rootPair); + } + else + { + resultsBuilder.AddDir(dirPair.ProcessedFileInfo); + } OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(dirPair.ProcessedFileInfo)); + // ── Process Purge candidates (destination-only files) ──────────────────── // ── Perform this first to clear space and also reduce run-time (avoid evaluating files that are copied into destination) if (dirPair.Destination.Exists) @@ -378,7 +387,7 @@ private async Task RunAsync(CancellationToken cancellationToken) if ((CopyOptions.Mirror || CopyOptions.Purge) && dirPair.IsExtra()) { - dirPair.Destination.Delete(true); + dirPair.Destination.Delete(false); continue; // source does not exist -> move to next dirpair } } @@ -395,47 +404,46 @@ private async Task RunAsync(CancellationToken cancellationToken) // Evaluate populates copier.ProcessedFileInfo (FileClass, Size, Name) // AND sets ShouldCopy / ShouldPurge based on this IRoboCommand's options. - EvaluateFilePair(copier); + if (EvaluateFilePair(copier) == IFilePairExtensions.EvaluationResult.SkippedByFilter) + continue; ProcessedFileInfo fileInfo = copier.ProcessedFileInfo; - if (copier.ShouldCopy) + if (!copier.ShouldCopy) { - if (listOnly) - { - OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); - progressReporter.AddFileCopied(fileInfo); - resultsBuilder.AddFileCopied(fileInfo); - } - else if (touchFiles) - { - dirPair.Destination.Create(); - if (copier.Destination.Exists is false) - copier.Destination.Create(); + // File was evaluated but not copied (skipped/extra/same/newer/older). + progressReporter.AddFileSkipped(fileInfo); + resultsBuilder.AddFileSkipped(fileInfo); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); + continue; + } + else if (listOnly) + { + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); + progressReporter.AddFileCopied(fileInfo); + resultsBuilder.AddFileCopied(fileInfo); + } + else if (touchFiles) + { + dirPair.Destination.Create(); + if (copier.Destination.Exists is false) + copier.Destination.Create(); - progressReporter.AddFileCopied(fileInfo); - resultsBuilder.AddFileCopied(fileInfo); - } - else - { - await multiThreadedController.WaitAsync(cancellationToken); - - // Announce the file before the transfer (mirrors Robocopy's pre-copy log line) - OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); - var task = PerformCopyOrMove(dirPair, copier, progressReporter, resultsBuilder, multiThreadedController, runningTasks, cancellationToken); - - if (task.Status < TaskStatus.RanToCompletion) - runningTasks[copier] = task; - else - await task; // acknowledge completion - } + progressReporter.AddFileCopied(fileInfo); + resultsBuilder.AddFileCopied(fileInfo); } else { - // File was evaluated but not copied (skipped/extra/same/newer/older). - progressReporter.AddFileSkipped(fileInfo); - resultsBuilder.AddFileSkipped(fileInfo); + await multiThreadedController.WaitAsync(cancellationToken); + + // Announce the file before the transfer (mirrors Robocopy's pre-copy log line) OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(fileInfo)); + var task = PerformCopyOrMove(dirPair, copier, progressReporter, resultsBuilder, multiThreadedController, runningTasks, cancellationToken); + + if (task.Status < TaskStatus.RanToCompletion) + runningTasks[copier] = task; + else + await task; // acknowledge completion } } } @@ -512,6 +520,7 @@ private async Task PerformCopyOrMove( /// private async IAsyncEnumerable EnumerateDirectoryPairsAsync(DirectoryPair root, int currentDepth, int maxDepth, [EnumeratorCancellation] CancellationToken cancellationToken) { + //yield return (root, currentDepth); yield return root; if (currentDepth >= maxDepth) @@ -548,7 +557,7 @@ private async IAsyncEnumerable EnumerateDirectoryPairsAsync(Dire /// The factory decides the copier implementation; we just enumerate source files /// and hand each pair to the factory. ///
- private async IAsyncEnumerable CreateFileCopiers(IDirectoryPair dirPair, [EnumeratorCancellation] CancellationToken cancellationToken) + private async IAsyncEnumerable CreateFileCopiers(DirectoryPair dirPair, [EnumeratorCancellation] CancellationToken cancellationToken) { IEnumerable sourceFiles = await Task.Run(() => dirPair.Source.EnumerateFiles("*", SearchOption.TopDirectoryOnly), cancellationToken) .ConfigureAwait(false); @@ -572,7 +581,7 @@ private async IAsyncEnumerable CreateFileCopiers(IDirectoryPair dir /// Creates purge-candidate instances for files that /// exist in the destination but not the source (i.e. "extra" files). /// - private async IAsyncEnumerable CreatePurgeCandidates(IDirectoryPair dirPair, [EnumeratorCancellation] CancellationToken cancellationToken) + private async IAsyncEnumerable CreatePurgeCandidates(DirectoryPair dirPair, [EnumeratorCancellation] CancellationToken cancellationToken) { if (!Directory.Exists(dirPair.Destination.FullName)) yield break; From 5ff2b85af79cd717be7da2bc6d81f348b4757f2e Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:07:01 -0400 Subject: [PATCH 10/16] Update Test App to allow testing new IRoboCommands --- RoboSharp.BackupApp/MainWindow.xaml | 12 ++++++++++- .../RoboSharp.BackupApp.csproj | 2 +- .../ViewModels/CommandGeneratorViewModel.cs | 13 ++++++++++-- .../ViewModels/MainWindowViewModel.cs | 20 ++++++++++++++++++- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/RoboSharp.BackupApp/MainWindow.xaml b/RoboSharp.BackupApp/MainWindow.xaml index 040e528e..af8b9de5 100644 --- a/RoboSharp.BackupApp/MainWindow.xaml +++ b/RoboSharp.BackupApp/MainWindow.xaml @@ -84,7 +84,17 @@ - + + + + + + + + + + + diff --git a/RoboSharp.BackupApp/RoboSharp.BackupApp.csproj b/RoboSharp.BackupApp/RoboSharp.BackupApp.csproj index cde4aa58..32b9654c 100644 --- a/RoboSharp.BackupApp/RoboSharp.BackupApp.csproj +++ b/RoboSharp.BackupApp/RoboSharp.BackupApp.csproj @@ -1,6 +1,6 @@  - net48;net8.0-Windows;net10.0-Windows + net8.0-Windows;net10.0-Windows WinExe false true diff --git a/RoboSharp.BackupApp/ViewModels/CommandGeneratorViewModel.cs b/RoboSharp.BackupApp/ViewModels/CommandGeneratorViewModel.cs index d1a036fe..aa788592 100644 --- a/RoboSharp.BackupApp/ViewModels/CommandGeneratorViewModel.cs +++ b/RoboSharp.BackupApp/ViewModels/CommandGeneratorViewModel.cs @@ -14,8 +14,9 @@ namespace RoboSharp.BackupApp.ViewModels { internal partial class CommandGeneratorViewModel : ObservableObject { - public CommandGeneratorViewModel() + public CommandGeneratorViewModel(IRoboCommandFactory factory) { + this.factory = factory; ResetOptions(); this.PropertyChanged += PropertyChangedHandler; System.Windows.Input.CommandManager.RequerySuggested += CommandManager_RequerySuggested; @@ -29,6 +30,8 @@ private void CommandManager_RequerySuggested(object sender, EventArgs e) this.BtnParseCommandOptionsCommand.NotifyCanExecuteChanged(); } + private readonly IRoboCommandFactory factory; + [ObservableProperty] private IRoboCommand _command; [ObservableProperty] private string runHoursStart; [ObservableProperty] private string runHoursEnd; @@ -46,7 +49,13 @@ private void ResetOptions() public IRoboCommand GetCommand() { UpdateCommandName(); - return new RoboCommand(Command); + var cmd = factory.GetRoboCommand(); + cmd.CopyOptions = Command.CopyOptions; + cmd.LoggingOptions = Command.LoggingOptions; + cmd.RetryOptions = Command.RetryOptions; + cmd.SelectionOptions = Command.SelectionOptions; + cmd.JobOptions.Merge(Command.JobOptions); + return cmd; } private void UpdateCommandName() diff --git a/RoboSharp.BackupApp/ViewModels/MainWindowViewModel.cs b/RoboSharp.BackupApp/ViewModels/MainWindowViewModel.cs index 8fa3e4f2..c7c66c5b 100644 --- a/RoboSharp.BackupApp/ViewModels/MainWindowViewModel.cs +++ b/RoboSharp.BackupApp/ViewModels/MainWindowViewModel.cs @@ -21,6 +21,7 @@ public MainWindowViewModel() SingleJobHistory = new JobHistoryViewModel(); BatchCommandViewModel = new BatchCommandViewModel(); System.Windows.Input.CommandManager.RequerySuggested += CommandManager_RequerySuggested; + CommandGenerator = new CommandGeneratorViewModel(CommandFactory); } private void CommandManager_RequerySuggested(object sender, EventArgs e) @@ -34,12 +35,29 @@ private void CommandManager_RequerySuggested(object sender, EventArgs e) } public RoboQueueViewModel RoboQueueViewModel { get; } = new RoboQueueViewModel(new RoboQueue("RoboQueue")); - public CommandGeneratorViewModel CommandGenerator { get; } = new CommandGeneratorViewModel(); + public CommandGeneratorViewModel CommandGenerator { get; } public CommandProgressViewModel SingleJobProgress { get; } = new CommandProgressViewModel(); public JobHistoryViewModel SingleJobHistory { get; } public BatchCommandViewModel BatchCommandViewModel { get; } + public CommandFactoryVM CommandFactory { get; } = new CommandFactoryVM(); + public class CommandFactoryVM : RoboSharp.RoboCommandFactory + { + public override IRoboCommand GetRoboCommand() + { + if (RoboCommand) return base.GetRoboCommand(); + if (RoboCommandPortable_Streamed) return new Extensions.RoboCommandPortable(Extensions.StreamedCopierFactory.DefaultFactory); + if (RoboCommandPortable_CopyFileEx) return new Extensions.RoboCommandPortable(new Extensions.Windows.CopyFileExFactory()); + if (RoboMover) return new Extensions.RoboMover(); + return base.GetRoboCommand(); + } + public bool RoboCommand { get; set; } = true; + public bool RoboCommandPortable_Streamed { get; set; } + public bool RoboCommandPortable_CopyFileEx { get; set; } + public bool RoboMover { get; set; } + } + #region < RoboCommand Buttons > From 52a23fee5c379805f2b5edb4529b020c4a33590d Mon Sep 17 00:00:00 2001 From: RBrenckman <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:05:40 -0400 Subject: [PATCH 11/16] WIP - Unit Testing --- .../RoboCommandPortableTests.cs | 220 ++++++++++++++---- .../RoboMoverTests.cs | 20 +- RoboSharp.Extensions.UnitTests/TestPrep.cs | 2 +- RoboSharp.Extensions/DirectoryPair.cs | 44 ++++ .../Options/CopyExtensions.cs | 6 +- RoboSharp.Extensions/RoboCommandPortable.cs | 109 +++++++-- RoboSharpUnitTesting/RoboCommandEventTests.cs | 47 ++-- 7 files changed, 351 insertions(+), 97 deletions(-) diff --git a/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs b/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs index b6abd9e0..51b185b5 100644 --- a/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs +++ b/RoboSharp.Extensions.UnitTests/RoboCommandPortableTests.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Testing.Platform.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; using RoboSharp.Extensions.Helpers; using RoboSharp.Extensions.Tests; using RoboSharp.Interfaces; @@ -7,6 +8,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.PortableExecutable; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -62,6 +64,12 @@ private static RoboCommandPortable GetCommand(RoboCommand rc, IFileCopierFactory }; } + [TestInitialize] + public void TestInit() + { + TestPrep.CleanDestination(); + } + [TestCleanup] public void TestCleanup() { @@ -78,33 +86,89 @@ public void IsAllowedDir(bool expected, string path) Assert.AreEqual(expected, RoboMover.IsAllowedRootDirectory(new DirectoryInfo(path))); } + private const CopyActionFlags Mov_ = CopyActionFlags.MoveFiles; + private const CopyActionFlags Move = CopyActionFlags.MoveFilesAndDirectories; + private const CopyActionFlags Copy = CopyActionFlags.Default; + private const CopyActionFlags CopySub = CopyActionFlags.CopySubdirectories; + private const CopyActionFlags CopyEmpty = CopyActionFlags.CopySubdirectoriesIncludingEmpty; + private const CopyActionFlags Purge = CopyActionFlags.Purge; + private const CopyActionFlags Mirror = CopyActionFlags.Mirror; + + private const LoggingFlags DefaultLogging = LoggingFlags.RoboSharpDefault | LoggingFlags.NoJobHeader; + private const LoggingFlags ReportExtra = DefaultLogging | LoggingFlags.ReportExtraFiles; + private const LoggingFlags Verbose = DefaultLogging | LoggingFlags.VerboseOutput; + private const LoggingFlags ListOnly = DefaultLogging | LoggingFlags.ListOnly; + private const LoggingFlags ListOnlyReportExtra = DefaultLogging | LoggingFlags.ListOnly | ReportExtra; + private const LoggingFlags ListOnlyVerbose = DefaultLogging | LoggingFlags.ListOnly | LoggingFlags.VerboseOutput; + /// - /// Copy Test will use a standard ROBOCOPY command + /// Run tests against the results of RoboCopy /// [TestMethod] + // copy items [Timeout(5000, CooperativeCancellation = true)] - [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Mirror }, DisplayName = "Mirror")] - [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.Default }, DisplayName = "Defaults")] - [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectories }, DisplayName = "Subdirectories")] - [DataRow(data: new object[] { DefaultLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectoriesIncludingEmpty }, DisplayName = "EmptySubdirectories")] - [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.Mirror }, DisplayName = "ListOnly-Mirror")] - [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.Default }, DisplayName = "ListOnly-Defaults")] - [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectories }, DisplayName = "ListOnly-Subdirectories")] - [DataRow(data: new object[] { ListOnlyLoggingAction, SelectionFlags.Default, CopyActionFlags.CopySubdirectoriesIncludingEmpty }, DisplayName = "ListOnly-EmptySubdirectories")] - public async Task CopyTest(object[] flags) + [DataRow(Copy, SelectionFlags.Default, DefaultLogging)] + [DataRow(CopySub, SelectionFlags.Default, DefaultLogging)] + [DataRow(CopyEmpty, SelectionFlags.Default, DefaultLogging)] + [DataRow(CopyEmpty, SelectionFlags.Default, Verbose)] + // List Only + [DataRow(Copy, SelectionFlags.Default, ListOnly)] + [DataRow(Copy, SelectionFlags.Default, ListOnlyVerbose)] + [DataRow(Copy, SelectionFlags.Default, ListOnlyReportExtra)] + [DataRow(CopySub, SelectionFlags.Default, ListOnly)] + [DataRow(CopySub, SelectionFlags.Default, ListOnlyVerbose)] + [DataRow(CopySub, SelectionFlags.Default, ListOnlyReportExtra)] + [DataRow(CopyEmpty, SelectionFlags.Default, ListOnly)] + [DataRow(CopyEmpty, SelectionFlags.Default, ListOnlyVerbose)] + [DataRow(CopyEmpty, SelectionFlags.Default, ListOnlyReportExtra)] + public Task CopyTests(CopyActionFlags copyFlags, SelectionFlags selectionFlags, LoggingFlags loggingFlags) + { + return RunCopyTest(copyFlags, loggingFlags, selectionFlags); + } + + [TestMethod] + [Timeout(5000, CooperativeCancellation = true)] + // copy items + [DataRow(CopyEmpty, DefaultLogging, SelectionFlags.Default)] + [DataRow(CopyEmpty, DefaultLogging, SelectionFlags.ExcludeNewer)] + [DataRow(CopyEmpty, DefaultLogging, SelectionFlags.ExcludeOlder)] + [DataRow(CopyEmpty, DefaultLogging, SelectionFlags.ExcludeChanged)] + [DataRow(CopyEmpty, DefaultLogging, SelectionFlags.ExcludeLonely)] + [DataRow(CopyEmpty, DefaultLogging, SelectionFlags.ExcludeExtra)] + [DataRow(CopyEmpty, DefaultLogging, SelectionFlags.IncludeSame)] + [DataRow(CopyEmpty, DefaultLogging, SelectionFlags.IncludeModified)] + public Task SelectionTests(CopyActionFlags copyFlags, LoggingFlags loggingFlags, SelectionFlags selectionFlags) + { + return RunCopyTest(copyFlags, loggingFlags, selectionFlags); + } + + [TestMethod] + [Timeout(5000, CooperativeCancellation = true)] + // copy items + [DataRow(0, Copy, ReportExtra)] + [DataRow(1, Copy, ReportExtra)] + [DataRow(2, Copy, ReportExtra)] + [DataRow(0, CopyEmpty, DefaultLogging)] + [DataRow(1, CopyEmpty, DefaultLogging)] + [DataRow(2, CopyEmpty, DefaultLogging)] + [DataRow(0, CopyEmpty, ReportExtra)] + [DataRow(1, CopyEmpty, ReportExtra)] + [DataRow(2, CopyEmpty, ReportExtra)] + public Task TestDepth(int depth, CopyActionFlags copyActionFlags, LoggingFlags loggingFlags) + { + return RunCopyTest(copyActionFlags, loggingFlags | ListOnly, default, depth); + } + + private async Task RunCopyTest(CopyActionFlags copyFlags, LoggingFlags loggingFlags, SelectionFlags selectionFlags, int maxDepth = 0) { try { - Test_Setup.ClearOutTestDestination(); - CopyActionFlags copyAction = (CopyActionFlags)flags[2]; - SelectionFlags selectionFlags = (SelectionFlags)flags[1]; - LoggingFlags loggingAction = (LoggingFlags)flags[0]; - - var rc = TestPrep.GetRoboCommand(false, copyAction, selectionFlags, loggingAction); + var rc = TestPrep.GetRoboCommand(false, copyFlags, selectionFlags, loggingFlags); var crc = GetCommand(rc); - Assert.AreEqual(loggingAction.HasFlag(LoggingFlags.ListOnly), rc.LoggingOptions.ListOnly); - Assert.AreEqual(loggingAction.HasFlag(LoggingFlags.ListOnly), crc.LoggingOptions.ListOnly); + bool listOnly = loggingFlags.HasFlag(LoggingFlags.ListOnly); + Assert.AreEqual(listOnly, rc.LoggingOptions.ListOnly); + Assert.AreEqual(listOnly, crc.LoggingOptions.ListOnly); TestContext.CancellationToken.Register(() => { @@ -112,7 +176,7 @@ public async Task CopyTest(object[] flags) crc.Stop(); }); - var results1 = await TestPrep.RunTests(rc, crc, !rc.LoggingOptions.ListOnly, TestContext.CancellationToken); + var results1 = await TestPrep.RunTests(rc, crc, !listOnly, TestContext.CancellationToken); TestPrep.CompareTestResults(results1[0], results1[1], rc.LoggingOptions.ListOnly); } catch (OperationCanceledException) when (TestContext.CancellationToken.IsCancellationRequested) @@ -121,6 +185,8 @@ public async Task CopyTest(object[] flags) + + private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags selectionFlags, LoggingFlags loggingFlags, out RoboCommand rc, out RoboCommandPortable rm) { rc = TestPrep.GetRoboCommand(false, copyFlags, selectionFlags, loggingFlags); @@ -129,10 +195,6 @@ private static void GetMoveCommands(CopyActionFlags copyFlags, SelectionFlags se } - - private const CopyActionFlags Mov_ = CopyActionFlags.MoveFiles; - private const CopyActionFlags Move = CopyActionFlags.MoveFilesAndDirectories; - /// /// This uses the actual logic provided by the RoboMover object /// @@ -217,7 +279,7 @@ static async Task CreateFile(CancellationToken token) } [TestMethod] - //[Timeout(5000, CooperativeCancellation = true)] + [Timeout(5000, CooperativeCancellation = true)] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files")] [DataRow(data: new object[] { Move, SelectionFlags.Default, DefaultLoggingAction }, DisplayName = "Move Files and Directories")] [DataRow(data: new object[] { Mov_, SelectionFlags.Default, DefaultLoggingAction | LoggingFlags.ListOnly }, DisplayName = "ListOnly | Move Files")] @@ -241,10 +303,11 @@ static async Task CreateFile(CancellationToken token) } [TestMethod] - [Timeout(5000, CooperativeCancellation = true)] + //[Timeout(5000, CooperativeCancellation = true)] // purge all - [DataRow(0, true, Mov_)] [DataRow(0, true, Move)] + [DataRow(0, true, Copy)] + [DataRow(0, true, CopyEmpty)] [DataRow(0, true, Mov_ | CopyActionFlags.CopySubdirectories)] [DataRow(0, true, Move | CopyActionFlags.CopySubdirectories)] [DataRow(0, true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] @@ -269,15 +332,29 @@ static async Task CreateFile(CancellationToken token) [DataRow(2, true, Mov_, LoggingFlags.ReportExtraFiles)] [DataRow(2, true, Mov_ | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] [DataRow(2, true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] - public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + // purge 3 + [DataRow(3, true, Move)] + [DataRow(3, true, Copy)] + [DataRow(3, true, CopyEmpty)] + // purge 3 + [DataRow(0, true, Purge)] + [DataRow(1, true, Purge)] + [DataRow(3, true, Purge)] + public async Task Test_Copy_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var implementation); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.Depth = depth; - cmd.LoggingOptions.ReportExtraFiles = true; - cmd.LoggingOptions.VerboseOutput = true; - await RunPurge(cmd, implementation, TestContext.CancellationToken); + Assert.AreSame(cmd.CopyOptions, implementation.CopyOptions); + await RunSelectionTests(cmd, implementation, TestContext.CancellationToken); + } + + private static async Task RunSelectionTests(RoboCommand cmd, RoboCommandPortable implementation, CancellationToken token) + { + //if (Test_Setup.IsRunningOnAppVeyor()) return; + var results = await TestPrep.RunTests(cmd, implementation, !cmd.LoggingOptions.ListOnly, TestPrep.CreateExtraDirectories, token); + TestPrep.CompareTestResults(results[0], results[1], cmd.LoggingOptions.ListOnly); } [TestMethod] @@ -288,12 +365,12 @@ public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, L [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] [DataRow(false, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(false, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] - public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) + public async Task Test_Selection_ExcludeFiles(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var implementation); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedFiles.Add("*0*_Bytes.txt"); - await RunPurge(cmd, implementation, TestContext.CancellationToken); + await RunSelectionTests(cmd, implementation, TestContext.CancellationToken); } [TestMethod] @@ -304,14 +381,14 @@ public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] - public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) + public async Task Test_Selection_ExcludeFolders(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var implementation); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder1"); // Top level empty cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder4"); // Bottom level empty cmd.SelectionOptions.ExcludedDirectories.Add("SubFolder_2a"); // folder with contents - await RunPurge(cmd, implementation, TestContext.CancellationToken); + await RunSelectionTests(cmd, implementation, TestContext.CancellationToken); } [TestMethod] @@ -328,23 +405,82 @@ public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) [DataRow(true, Move | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] - public async Task Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + public async Task Test_Selection_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.FileFilter = new string[] { "*0*_Bytes.txt" }; - await RunPurge(cmd, mover, TestContext.CancellationToken); + await RunSelectionTests(cmd, mover, TestContext.CancellationToken); } - private static async Task RunPurge(RoboCommand cmd, RoboCommandPortable implementation, CancellationToken token) + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + [DataRow(CopyActionFlags.MoveFiles)] + [DataRow(CopyActionFlags.MoveFiles | CopyActionFlags.Purge)] + [DataRow(CopyActionFlags.MoveFilesAndDirectories)] + [DataRow(CopyActionFlags.MoveFilesAndDirectories | CopyActionFlags.Purge)] + public async Task PurgeTests(CopyActionFlags copyOptions) { - //if (Test_Setup.IsRunningOnAppVeyor()) return; - var results = await TestPrep.RunTests(cmd, implementation, !cmd.LoggingOptions.ListOnly, TestPrep.CreateFilesToPurge, token); - TestPrep.CompareTestResults(results[0], results[1], cmd.LoggingOptions.ListOnly); - } + GetMoveCommands( + CopyActionFlags.CopySubdirectoriesIncludingEmpty | copyOptions, + SelectionFlags.Default, + DefaultLoggingAction, + out _, out var rm); + Test_Setup.ClearOutTestDestination(); + await TestPrep.PrepMoveFiles(TestContext.CancellationToken); + + string subfolderpath = @"SubFolder_1\SubFolder_1.1\SubFolder_1.2"; + FilePair[] SourceFiles = new FilePair[] { + new FilePair(Path.Combine(rm.CopyOptions.Source, "4_Bytes.txt"), Path.Combine(rm.CopyOptions.Destination, "4_Bytes.txt")), + new FilePair(Path.Combine(rm.CopyOptions.Source, "1024_Bytes.txt"), Path.Combine(rm.CopyOptions.Destination, "1024_Bytes.txt")), + new FilePair(Path.Combine(rm.CopyOptions.Source, subfolderpath, "0_Bytes.txt"), Path.Combine(rm.CopyOptions.Destination, subfolderpath, "0_Bytes.txt")), + new FilePair(Path.Combine(rm.CopyOptions.Source, subfolderpath, "4_Bytes.htm"), Path.Combine(rm.CopyOptions.Destination, subfolderpath, "4_Bytes.htm")), + }; + FileInfo[] purgeFiles = new FileInfo[] + { + new FileInfo(Path.Combine(rm.CopyOptions.Destination, "PurgeFile_1.txt")), + new FileInfo(Path.Combine(rm.CopyOptions.Destination, "PurgeFile_2.txt")), + new FileInfo(Path.Combine(rm.CopyOptions.Destination, "PurgeFolder_1", "PurgeFile_3.txt")), + new FileInfo(Path.Combine(rm.CopyOptions.Destination, "PurgeFolder_2", "SubFolder","PurgeFile_4.txt")), + }; + DirectoryInfo[] PurgeDirectories = new DirectoryInfo[] + { + purgeFiles[2].Directory, + purgeFiles[3].Directory, + purgeFiles[3].Directory.Parent, + }; + foreach (var dir in PurgeDirectories) Directory.CreateDirectory(dir.FullName); + foreach (var file in purgeFiles) File.WriteAllText(file.FullName, "PURGE ME"); + await rm.Start(); + foreach (var lin in rm.GetResults().LogLines) + Console.WriteLine(lin); + + bool purge = rm.CopyOptions.Purge; + // Evaluate purged + foreach (var file in purgeFiles) + { + file.Refresh(); + Assert.AreEqual(purge, !file.Exists, purge ? "File was not purged." : "File was purged unexpectedly."); + } + foreach (var dir in PurgeDirectories) + { + dir.Refresh(); + Assert.AreEqual(purge, !dir.Exists, purge ? "Directory was not purged." : "Directory was purged unexpectedly."); + } + //evaluate moved + foreach (var filepair in SourceFiles) + { + filepair.Refresh(); + Assert.IsTrue(filepair.IsExtra(), string.Format("\nSource:{0}\nDestination:{1}\nFile was not moved to destination directory.", filepair.Source, filepair.Destination)); + } + bool moveDirectories = rm.CopyOptions.MoveFilesAndDirectories; + Assert.AreEqual(moveDirectories, SourceFiles[2].Parent.IsExtra(), moveDirectories ? "Directory was not moved" : "Directory was moved unexpectedly."); + Assert.AreEqual(moveDirectories, SourceFiles[3].Parent.IsExtra(), moveDirectories ? "Directory was not moved" : "Directory was moved unexpectedly."); + + } } } #endif \ No newline at end of file diff --git a/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs b/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs index 80d4059f..3b289d0b 100644 --- a/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs +++ b/RoboSharp.Extensions.UnitTests/RoboMoverTests.cs @@ -232,13 +232,13 @@ static async Task CreateFile(CancellationToken token) [DataRow(2, true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] [TestMethod] [Timeout(10000, CooperativeCancellation = true)] - public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + public async Task Move_Depth(int depth, bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.Depth = depth; - await RunPurge(cmd, mover, TestContext.CancellationToken); + await RoboMoverTests.RunMoveTest(cmd, mover, TestContext.CancellationToken); } [TestMethod] @@ -249,12 +249,12 @@ public async Task Purge_Depth(int depth, bool listOnly, CopyActionFlags flags, L [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] [DataRow(false, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(false, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] - public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) + public async Task Move_ExcludeFiles(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedFiles.Add("*0*_Bytes.txt"); - await RunPurge(cmd, mover, TestContext.CancellationToken); + await RoboMoverTests.RunMoveTest(cmd, mover, TestContext.CancellationToken); } [TestMethod] @@ -265,14 +265,14 @@ public async Task Purge_ExcludeFiles(bool listOnly, CopyActionFlags flags) [DataRow(true, Move | CopyActionFlags.CopySubdirectories)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty)] - public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) + public async Task Move_ExcludeFolders(bool listOnly, CopyActionFlags flags) { GetMoveCommands(flags, SelectionFlags.Default, DefaultLoggingAction, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder1"); // Top level empty cmd.SelectionOptions.ExcludedDirectories.Add("EmptyFolder4"); // Bottom level empty cmd.SelectionOptions.ExcludedDirectories.Add("SubFolder_2a"); // folder with contents - await RunPurge(cmd, mover, TestContext.CancellationToken); + await RoboMoverTests.RunMoveTest(cmd, mover, TestContext.CancellationToken); } [TestMethod] @@ -289,19 +289,19 @@ public async Task Purge_ExcludeFolders(bool listOnly, CopyActionFlags flags) [DataRow(true, Move | CopyActionFlags.CopySubdirectories, LoggingFlags.ReportExtraFiles)] [DataRow(true, Mov_ | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] [DataRow(true, Move | CopyActionFlags.CopySubdirectoriesIncludingEmpty, LoggingFlags.ReportExtraFiles)] - public async Task Purge_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) + public async Task Move_IncludedFiles(bool listOnly, CopyActionFlags flags, LoggingFlags? loggs = null) { LoggingFlags log = loggs.HasValue ? loggs.Value | DefaultLoggingAction : DefaultLoggingAction; GetMoveCommands(flags, SelectionFlags.Default, log, out var cmd, out var mover); cmd.LoggingOptions.ListOnly = listOnly; cmd.CopyOptions.FileFilter = new string[] { "*0*_Bytes.txt" }; - await RunPurge(cmd, mover, TestContext.CancellationToken); + await RoboMoverTests.RunMoveTest(cmd, mover, TestContext.CancellationToken); } - private async Task RunPurge(RoboCommand cmd, RoboMover mover, CancellationToken token) + private static async Task RunMoveTest(RoboCommand cmd, RoboMover mover, CancellationToken token) { //if (Test_Setup.IsRunningOnAppVeyor()) return; - var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, TestPrep.CreateFilesToPurge, token); + var results = await TestPrep.RunTests(cmd, mover, !cmd.LoggingOptions.ListOnly, TestPrep.CreateExtraDirectories, token); TestPrep.CompareTestResults(results[0], results[1], cmd.LoggingOptions.ListOnly); } diff --git a/RoboSharp.Extensions.UnitTests/TestPrep.cs b/RoboSharp.Extensions.UnitTests/TestPrep.cs index 728a4fe6..8e86e8ef 100644 --- a/RoboSharp.Extensions.UnitTests/TestPrep.cs +++ b/RoboSharp.Extensions.UnitTests/TestPrep.cs @@ -247,7 +247,7 @@ public static async Task PrepMoveFiles(CancellationToken token) ); } - public static async Task CreateFilesToPurge(CancellationToken token) + public static async Task CreateExtraDirectories(CancellationToken token) { await PrepMoveFiles(token); token.ThrowIfCancellationRequested(); diff --git a/RoboSharp.Extensions/DirectoryPair.cs b/RoboSharp.Extensions/DirectoryPair.cs index 3431e743..b55d5590 100644 --- a/RoboSharp.Extensions/DirectoryPair.cs +++ b/RoboSharp.Extensions/DirectoryPair.cs @@ -80,6 +80,50 @@ public DirectoryPair(DirectoryInfo source, DirectoryInfo destination) /// Refresh this via public CachedEnumerable SourceDirectories => lazySourceDirs.Value; +#if NET6_0_OR_GREATER + /// + /// Gets a direct descendant of this node + /// + /// + /// + public DirectoryPair CreateChild(DirectoryInfo child) + { + string path; + if (child.FullName.StartsWith(Source.FullName)) + { + path = Path.Combine(Destination.FullName, Path.GetRelativePath(Source.FullName, child.FullName)); + return new DirectoryPair(child, new DirectoryInfo(path)); + } + if (child.FullName.StartsWith(Destination.FullName)) + { + path = Path.Combine(Source.FullName, Path.GetRelativePath(Destination.FullName, child.FullName)); + return new DirectoryPair(child, new DirectoryInfo(path)); + } + throw new InvalidOperationException($"Directory '{child.FullName}' is not a child of this DirectoryPair"); + } + + /// + /// Gets a direct descendant of this node + /// + /// + /// + public FilePair CreateChild(FileInfo child) + { + string path; + if (child.FullName.StartsWith(Source.FullName)) + { + path = Path.Combine(Destination.FullName, Path.GetRelativePath(Source.FullName, child.FullName)); + return new FilePair(child, new FileInfo(path)); + } + if (child.FullName.StartsWith(Destination.FullName)) + { + path = Path.Combine(Source.FullName, Path.GetRelativePath(Destination.FullName, child.FullName)); + return new FilePair(child, new FileInfo(path)); + } + throw new InvalidOperationException($"Directory '{child.FullName}' is not a child of this DirectoryPair"); + } +#endif + /// public void Refresh() { diff --git a/RoboSharp.Extensions/Options/CopyExtensions.cs b/RoboSharp.Extensions/Options/CopyExtensions.cs index a649bf47..f30d5d0e 100644 --- a/RoboSharp.Extensions/Options/CopyExtensions.cs +++ b/RoboSharp.Extensions/Options/CopyExtensions.cs @@ -52,9 +52,9 @@ public static bool IsPurging(this CopyActionFlags flag) => /// /// Evaluates the to check if any of the PURGE options are enabled /// - public static bool IsPurging(this CopyOptions options) => - options.Purge || - options.Mirror; + public static bool IsPurging(this IRoboCommand command) => + command.SelectionOptions.ExcludeExtra is false + && (command.CopyOptions.Purge || command.CopyOptions.Mirror); /// /// Compare the current depth against the maximum allowed depth, and determine if directory recursion can continue. diff --git a/RoboSharp.Extensions/RoboCommandPortable.cs b/RoboSharp.Extensions/RoboCommandPortable.cs index b74748de..02caf354 100644 --- a/RoboSharp.Extensions/RoboCommandPortable.cs +++ b/RoboSharp.Extensions/RoboCommandPortable.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Threading; @@ -290,7 +291,13 @@ private async Task RunAsync(CancellationToken cancellationToken) var rootPair = new DirectoryPair(this.CopyOptions.Source, this.CopyOptions.Destination); bool listOnly = LoggingOptions.ListOnly; bool touchFiles = CopyOptions.CreateDirectoryAndFileTree; + bool purging = !SelectionOptions.ExcludeExtra && (CopyOptions.Purge || CopyOptions.Mirror); + bool reportExtraFiles = (!SelectionOptions.ExcludeExtra || (purging && CopyOptions.Depth != 1) ) && (LoggingOptions.VerboseOutput || LoggingOptions.ReportExtraFiles); + // !!!! -- TODO : FIX REPORTeXTRAdIRS -- THIS IS NOT WORKING YET --- + // Something to do with maxdepth == 1 & all other conditions... + bool reportExtraDirs = maxDepth != 1 && !(!purging && !(CopyOptions.IsRecursive()) && !LoggingOptions.ReportExtraFiles && !CopyOptions.HasDefaultFileFilter()); + //reportExtraFiles || (CopyOptions.CopySubdirectories || CopyOptions.CopySubdirectoriesIncludingEmpty || CopyOptions.MoveFilesAndDirectories); SemaphoreSlim multiThreadedController = new SemaphoreSlim(CopyOptions.MultiThreadedCopiesCount >= 128 ? 128 : CopyOptions.MultiThreadedCopiesCount <= 1 ? 1 : CopyOptions.MultiThreadedCopiesCount); Dictionary infoDict = new(); @@ -303,13 +310,14 @@ private async Task RunAsync(CancellationToken cancellationToken) // Robocopy reports totals before starting transfers; we replicate that here // so ProgressEstimator can give accurate percentage estimates from the start. - await foreach (DirectoryPair dirPair in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) + await foreach ((DirectoryPair dirPair, _) in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) { // Tell the estimator a directory exists on the source side EvaluateDirPair(dirPair); - progressReporter.AddDir(dirPair.ProcessedFileInfo); - - infoDict[dirPair.Source.FullName] = dirPair.ProcessedFileInfo; + //if (depth > maxDepth) + // dirPair.ProcessedFileInfo.SetDirectoryClass(ProcessedDirectoryFlag.ExtraDir, Configuration); + + infoDict[dirPair.Destination.FullName] = dirPair.ProcessedFileInfo; await foreach (IFileCopier copier in CreateFileCopiers(dirPair, cancellationToken)) { dirPair.ProcessedFileInfo.Size++; @@ -317,20 +325,18 @@ private async Task RunAsync(CancellationToken cancellationToken) } // ── Pass 2: process each directory ─────────────────────────────────────── - await foreach (var dirPair in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) - //(DirectoryPair dirPair, int currentDepth) - await foreach (DirectoryPair dirPair in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) + await foreach ((DirectoryPair dirPair, int currentDepth) in EnumerateDirectoryPairsAsync(rootPair, 1, maxDepth, cancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); - if (infoDict.TryGetValue(dirPair.Source.FullName, out var pInfo)) + if (infoDict.TryGetValue(dirPair.Destination.FullName, out var pInfo)) { dirPair.ProcessedFileInfo = pInfo; - infoDict.Remove(dirPair.Source.FullName); // key will never be read again } else { EvaluateDirPair(dirPair); + infoDict[dirPair.Destination.FullName] = dirPair.ProcessedFileInfo; } progressReporter.AddDir(dirPair.ProcessedFileInfo); @@ -349,6 +355,13 @@ private async Task RunAsync(CancellationToken cancellationToken) // ── Perform this first to clear space and also reduce run-time (avoid evaluating files that are copied into destination) if (dirPair.Destination.Exists) { + if (purging && dirPair.IsExtra()) + { + dirPair.Destination.Delete(true); + continue; // source does not exist -> move to next dirpair + } + + // Report or Purge extra files await foreach (IFileCopier purgeCopier in CreatePurgeCandidates(dirPair, cancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -385,17 +398,41 @@ private async Task RunAsync(CancellationToken cancellationToken) } } - if ((CopyOptions.Mirror || CopyOptions.Purge) && dirPair.IsExtra()) + // Detect Extra Directories + if (true || currentDepth <= maxDepth) { - dirPair.Destination.Delete(false); - continue; // source does not exist -> move to next dirpair + foreach (var child in Directory.EnumerateDirectories(dirPair.Destination.FullName, "*", SearchOption.TopDirectoryOnly)) + { + // check if dictionary contains the key + if (infoDict.ContainsKey(child)) + continue; + + // not part of source tree: + if (reportExtraDirs) + { + var info = new ProcessedFileInfo(child, FileClassType.NewDir, fileClass: Configuration.LogParsing_ExtraDir, purging ? -1 : 0); + resultsBuilder.AddDir(info); + } + + if (purging) + { + try + { + Directory.Delete(child, true); + } + catch (Exception e) + { + OnCommandError?.Invoke(this, new CommandErrorEventArgs($"Unable to purge directory : {child}", e)); + } + } + } } } // ── Process Source files for copy/move ────────────────────────────────────────────────── if (dirPair.Source.Exists) { - if (includeEmpty) + if (includeEmpty && !listOnly) dirPair.Destination.Create(); await foreach (IFileCopier copier in CreateFileCopiers(dirPair, cancellationToken)) @@ -514,14 +551,50 @@ private async Task PerformCopyOrMove( } } + /// Processes an EXTRA directory tree from the destination, potentially purging it. + private void ProcessExtraDirectory(DirectoryPair pair, int currentDepth, ResultsBuilder resultsBuilder) + { + if (!pair.Destination.Exists) return; + bool shouldPurge = CopyOptions.Purge && this.ShouldPurge(pair); + + // This gets it to pass unit tests, but *feels* wrong + if (!shouldPurge && !CopyOptions.IsRecursive() && !LoggingOptions.ReportExtraFiles && !CopyOptions.HasDefaultFileFilter()) return; + + if (pair.ProcessedFileInfo is null) + pair.ProcessedFileInfo = new ProcessedFileInfo(directory: pair.Destination, this, ProcessedDirectoryFlag.ExtraDir, size: -1); + + resultsBuilder.AddDir(pair.ProcessedFileInfo); + if (!shouldPurge) return; + + ////Process Files + //IEnumerable files = pair.DestinationFiles; + //foreach (var file in files) + //{ + // if (cancelRequest.IsCancellationRequested) break; + // ProcessExtraFile(file); + //} + + //// Dig into subdirectories + //if (PairEvaluator.CanDigDeeper(currentDepth)) + //{ + // foreach (var dir in pair.ExtraDirectories) + // { + // if (cancelRequest.IsCancellationRequested) break; + // ProcessExtraDirectory(dir, currentDepth + 1); + // } + //} + + // Delete the current directory + + } + /// - /// Yields the root pair and (if recurse is true) all sub-directory pairs, - /// mirroring Robocopy's directory tree walk. + /// Yields the root pair and (if recurse is true) all sub-directory pairs, mirroring Robocopy's directory tree walk. + ///
Only yields items from the Source tree ///
- private async IAsyncEnumerable EnumerateDirectoryPairsAsync(DirectoryPair root, int currentDepth, int maxDepth, [EnumeratorCancellation] CancellationToken cancellationToken) + private async IAsyncEnumerable<(DirectoryPair dirPair, int currentDepth)> EnumerateDirectoryPairsAsync(DirectoryPair root, int currentDepth, int maxDepth, [EnumeratorCancellation] CancellationToken cancellationToken) { - //yield return (root, currentDepth); - yield return root; + yield return (root, currentDepth); if (currentDepth >= maxDepth) yield break; diff --git a/RoboSharpUnitTesting/RoboCommandEventTests.cs b/RoboSharpUnitTesting/RoboCommandEventTests.cs index b699483d..d5f06d7b 100644 --- a/RoboSharpUnitTesting/RoboCommandEventTests.cs +++ b/RoboSharpUnitTesting/RoboCommandEventTests.cs @@ -9,47 +9,48 @@ namespace RoboSharp.UnitTests [TestClass] public class RoboCommandEventTests { - private static void RunTestThenAssert(IRoboCommand cmd, ref bool EventBool) + private static async Task RunTestThenAssert(IRoboCommand cmd, Func wasRaised) { - var results = cmd.StartAsync().GetAwaiter().GetResult(); + Console.WriteLine($"Type of command : {cmd.GetType()}"); + var results = await cmd.StartAsync(); Test_Setup.WriteLogLines(results); - if (!EventBool) throw new AssertFailedException("Subscribed Event was not Raised!"); + if (!wasRaised()) throw new AssertFailedException("Subscribed Event was not Raised!"); } /// protected virtual IRoboCommand GenerateCommand(bool UseLargerFileSet, bool ListOnlyMode) => UnitTests.Test_Setup.GenerateCommand(UseLargerFileSet, ListOnlyMode); [TestMethod] - public void RoboCommand_OnCommandCompleted() + public virtual async Task RoboCommand_OnCommandCompleted() { var cmd = GenerateCommand(false, true); bool TestPassed = false; cmd.OnCommandCompleted += (o, e) => TestPassed = true; - RunTestThenAssert(cmd, ref TestPassed); + await RunTestThenAssert(cmd, () => TestPassed); } [TestMethod] - public void RoboCommand_OnCommandError() + public virtual async Task RoboCommand_OnCommandError() { var cmd = GenerateCommand(false, true); cmd.CopyOptions.Source += "FolderDoesNotExist"; bool TestPassed = false; cmd.OnCommandError += (o, e) => TestPassed = true; - RunTestThenAssert(cmd, ref TestPassed); + await RunTestThenAssert(cmd, () => TestPassed); } [TestMethod] - public void RoboCommand_OnCopyProgressChanged() + public virtual async Task RoboCommand_OnCopyProgressChanged() { Test_Setup.ClearOutTestDestination(); var cmd = GenerateCommand(false, false); bool TestPassed = false; cmd.OnCopyProgressChanged += (o, e) => TestPassed = true; - RunTestThenAssert(cmd, ref TestPassed); + await RunTestThenAssert(cmd, () => TestPassed); } [TestMethod] - public void RoboCommand_OnError() + public virtual async Task RoboCommand_OnError() { if (Test_Setup.IsRunningOnAppVeyor()) return; @@ -63,37 +64,37 @@ public void RoboCommand_OnError() { f.WriteLine("StartTest!"); Console.WriteLine("Expecting 1 File Failed!\n\n"); - RunTestThenAssert(cmd, ref TestPassed); + await RunTestThenAssert(cmd, () => TestPassed); } } [TestMethod] - public void RoboCommand_OnFileProcessed() + public virtual async Task RoboCommand_OnFileProcessed() { Test_Setup.ClearOutTestDestination(); var cmd = GenerateCommand(false, true); bool TestPassed = false; cmd.OnFileProcessed += (o, e) => TestPassed = true; - RunTestThenAssert(cmd, ref TestPassed); + await RunTestThenAssert(cmd, () => TestPassed); } [TestMethod] - public void RoboCommand_ProgressEstimatorCreated() + public virtual async Task RoboCommand_ProgressEstimatorCreated() { var cmd = GenerateCommand(false, true); bool TestPassed = false; cmd.OnProgressEstimatorCreated += (o, e) => TestPassed = true; - RunTestThenAssert(cmd, ref TestPassed); + await RunTestThenAssert(cmd, () => TestPassed); } - //[TestMethod] //TODO: Unsure how to force the TaskFaulted Unit test, as it should never actually occurr..... - public void RoboCommand_TaskFaulted() - { - var cmd = GenerateCommand(false, true); - bool TestPassed = false; - cmd.TaskFaulted += (o, e) => TestPassed = true; - RunTestThenAssert(cmd, ref TestPassed); - } + ////[TestMethod] //TODO: Unsure how to force the TaskFaulted Unit test, as it should never actually occurr..... + //public virtual async Task RoboCommand_TaskFaulted() + //{ + // var cmd = GenerateCommand(false, true); + // bool TestPassed = false; + // cmd.TaskFaulted += (o, e) => TestPassed = true; + // await RunTestThenAssert(cmd, () => TestPassed); + //} } } From 516e6e4f4963dde023e067d88e5b7cfcad386f94 Mon Sep 17 00:00:00 2001 From: RFBomb <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:28:20 -0400 Subject: [PATCH 12/16] Create Build-And-Test action --- .github/workflows/build-and-test.yml | 76 ++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/build-and-test.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..60b40699 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,76 @@ +# +# Build the solution, run the 2 unit tests, then create artifacts. +# +# For more information on GitHub Actions, refer to https://github.com/features/actions +# For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications, +# refer to https://github.com/microsoft/github-actions-for-desktop-apps + +name: Build and Test + +on: + push: + branches: [ "dev" ] + pull_request: + branches: [ "dev" ] + +jobs: + + build: + + strategy: + matrix: + configuration: [Release] + + runs-on: windows-latest # For a list of available runner types, refer to + # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on + + env: + Solution_Name: RoboSharp.sln # Replace with your solution name, i.e. MyWpfApp.sln. + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # Install the .NET SDK - https://github.com/actions/setup-dotnet + - name: Install .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 8.0.x + 10.0.x + + # Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild + - name: Setup MSBuild.exe + uses: microsoft/setup-msbuild@v3 + + - name: Restore + run: dotnet restore $env:Solution_Name + + - name: Build + run: dotnet build $env:Solution_Name --configuration ${{ matrix.configuration }} --no-restore + + - name: Run RoboSharp Tests + run: dotnet test ./RoboSharpUnitTesting/RoboSharpUnitTesting.csproj --configuration Release --no-build + + - name: Run RoboSharp.Extensions Tests + run: dotnet test ./RoboSharp.Extensions.UnitTests/RoboSharp.Extensions.UnitTests.csproj --configuration Release --no-build + + # Execute all unit tests in the solution + - name: Execute unit tests + run: dotnet test + + - name: Gather Artifacts + run: | + dotnet pack ./RoboSharp/RoboSharp.csproj --configuration Release --no-build --output ./artifacts + dotnet pack ./RoboSharp.Extensions/RoboSharp.Extensions.csproj --configuration Release --no-build --output ./artifacts + dotnet publish ./Robosharp.ConsoleApp/Robosharp.ConsoleApp.csproj --configuration Release --no-build --output ./artifacts/ConsoleApp + dotnet publish ./RoboSharp.BackupApp/RoboSharp.BackupApp.csproj --configuration Release --no-build --output ./artifacts/BackupApp + + + - name: Upload Artifacts + uses: actions/upload-artifact@v6 + with: + name: nuget-packages + path: ./artifacts/* \ No newline at end of file From 78a7811aab2f3518836d56f90f7de5c88a7f0538 Mon Sep 17 00:00:00 2001 From: RFBomb <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:30:52 -0400 Subject: [PATCH 13/16] Update build-and-test.yml --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 60b40699..227e07c2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -9,7 +9,7 @@ name: Build and Test on: push: - branches: [ "dev" ] + branches: [ "dev", "Extensions.Mocks" ] pull_request: branches: [ "dev" ] From d5de5e2ede2bf4c91660cc1bfeb273ba334279ab Mon Sep 17 00:00:00 2001 From: Robert Brenckman <20431767+RFBomb@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:22:31 -0400 Subject: [PATCH 14/16] Update RoboCommandPortable.cs Claude Suggested Fixes --- RoboSharp.Extensions/RoboCommandPortable.cs | 179 +++++++------------- 1 file changed, 65 insertions(+), 114 deletions(-) diff --git a/RoboSharp.Extensions/RoboCommandPortable.cs b/RoboSharp.Extensions/RoboCommandPortable.cs index 02caf354..8ed00016 100644 --- a/RoboSharp.Extensions/RoboCommandPortable.cs +++ b/RoboSharp.Extensions/RoboCommandPortable.cs @@ -1,4 +1,4 @@ -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER using RoboSharp.EventArgObjects; using RoboSharp.Extensions.Helpers; @@ -293,12 +293,8 @@ private async Task RunAsync(CancellationToken cancellationToken) bool touchFiles = CopyOptions.CreateDirectoryAndFileTree; bool purging = !SelectionOptions.ExcludeExtra && (CopyOptions.Purge || CopyOptions.Mirror); bool reportExtraFiles = (!SelectionOptions.ExcludeExtra || (purging && CopyOptions.Depth != 1) ) && (LoggingOptions.VerboseOutput || LoggingOptions.ReportExtraFiles); - - // !!!! -- TODO : FIX REPORTeXTRAdIRS -- THIS IS NOT WORKING YET --- - // Something to do with maxdepth == 1 & all other conditions... - bool reportExtraDirs = maxDepth != 1 && !(!purging && !(CopyOptions.IsRecursive()) && !LoggingOptions.ReportExtraFiles && !CopyOptions.HasDefaultFileFilter()); - //reportExtraFiles || (CopyOptions.CopySubdirectories || CopyOptions.CopySubdirectoriesIncludingEmpty || CopyOptions.MoveFilesAndDirectories); - + bool reportExtraDirs = !SelectionOptions.ExcludeExtra; + SemaphoreSlim multiThreadedController = new SemaphoreSlim(CopyOptions.MultiThreadedCopiesCount >= 128 ? 128 : CopyOptions.MultiThreadedCopiesCount <= 1 ? 1 : CopyOptions.MultiThreadedCopiesCount); Dictionary infoDict = new(); ConcurrentDictionary runningTasks = new(); @@ -353,81 +349,30 @@ private async Task RunAsync(CancellationToken cancellationToken) // ── Process Purge candidates (destination-only files) ──────────────────── // ── Perform this first to clear space and also reduce run-time (avoid evaluating files that are copied into destination) - if (dirPair.Destination.Exists) - { - if (purging && dirPair.IsExtra()) - { - dirPair.Destination.Delete(true); - continue; // source does not exist -> move to next dirpair - } + // Detect Extra Directories (dest dirs not in source tree) +if (dirPair.Destination.Exists) +{ + foreach (var child in Directory.EnumerateDirectories(dirPair.Destination.FullName, "*", SearchOption.TopDirectoryOnly)) + { + if (infoDict.ContainsKey(child)) + continue; // part of source tree, already handled - // Report or Purge extra files - await foreach (IFileCopier purgeCopier in CreatePurgeCandidates(dirPair, cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); + // This directory exists in dest but not source — it's Extra + if (reportExtraDirs || purging) + { + var extraInfo = new ProcessedFileInfo(child, FileClassType.NewDir, + fileClass: Configuration.LogParsing_ExtraDir, purging ? -1 : 0); + resultsBuilder.AddDir(extraInfo); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(extraInfo)); + } - EvaluateFilePair(purgeCopier); - ProcessedFileInfo purgeInfo = purgeCopier.ProcessedFileInfo; + if (purging) + { + PurgeExtraDirectory(child, resultsBuilder, cancellationToken); + } + } +} - if (purgeCopier.ShouldPurge) - { - OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(purgeInfo)); - - try - { - purgeCopier.Destination.Delete(); - progressReporter.AddFileExtra(purgeInfo); - resultsBuilder.AddFilePurged(purgeInfo); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - resultsBuilder.AddFileFailed(purgeInfo); - OnCommandError?.Invoke(this, new CommandErrorEventArgs(ex.Message, ex)); - } - } - else - { - // Extra file is present but purge is disabled — treat as skipped/extra - progressReporter.AddFileExtra(purgeInfo); - resultsBuilder.AddFileExtra(purgeInfo); - OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(purgeInfo)); - } - } - - // Detect Extra Directories - if (true || currentDepth <= maxDepth) - { - foreach (var child in Directory.EnumerateDirectories(dirPair.Destination.FullName, "*", SearchOption.TopDirectoryOnly)) - { - // check if dictionary contains the key - if (infoDict.ContainsKey(child)) - continue; - - // not part of source tree: - if (reportExtraDirs) - { - var info = new ProcessedFileInfo(child, FileClassType.NewDir, fileClass: Configuration.LogParsing_ExtraDir, purging ? -1 : 0); - resultsBuilder.AddDir(info); - } - - if (purging) - { - try - { - Directory.Delete(child, true); - } - catch (Exception e) - { - OnCommandError?.Invoke(this, new CommandErrorEventArgs($"Unable to purge directory : {child}", e)); - } - } - } - } - } // ── Process Source files for copy/move ────────────────────────────────────────────────── if (dirPair.Source.Exists) @@ -551,42 +496,48 @@ private async Task PerformCopyOrMove( } } - /// Processes an EXTRA directory tree from the destination, potentially purging it. - private void ProcessExtraDirectory(DirectoryPair pair, int currentDepth, ResultsBuilder resultsBuilder) - { - if (!pair.Destination.Exists) return; - bool shouldPurge = CopyOptions.Purge && this.ShouldPurge(pair); - - // This gets it to pass unit tests, but *feels* wrong - if (!shouldPurge && !CopyOptions.IsRecursive() && !LoggingOptions.ReportExtraFiles && !CopyOptions.HasDefaultFileFilter()) return; - - if (pair.ProcessedFileInfo is null) - pair.ProcessedFileInfo = new ProcessedFileInfo(directory: pair.Destination, this, ProcessedDirectoryFlag.ExtraDir, size: -1); - - resultsBuilder.AddDir(pair.ProcessedFileInfo); - if (!shouldPurge) return; - - ////Process Files - //IEnumerable files = pair.DestinationFiles; - //foreach (var file in files) - //{ - // if (cancelRequest.IsCancellationRequested) break; - // ProcessExtraFile(file); - //} - - //// Dig into subdirectories - //if (PairEvaluator.CanDigDeeper(currentDepth)) - //{ - // foreach (var dir in pair.ExtraDirectories) - // { - // if (cancelRequest.IsCancellationRequested) break; - // ProcessExtraDirectory(dir, currentDepth + 1); - // } - //} - - // Delete the current directory + /// +/// Recursively reports and deletes an extra destination directory and all its contents. +/// Mirrors RoboCopy's behaviour: files are counted as Extra/Purged before the directory is deleted. +/// +private void PurgeExtraDirectory(string destDir, ResultsBuilder resultsBuilder, CancellationToken cancellationToken) +{ + cancellationToken.ThrowIfCancellationRequested(); + + // Report and count each file inside the extra directory before deleting + foreach (var file in Directory.EnumerateFiles(destDir, "*", SearchOption.TopDirectoryOnly)) + { + var fileInfo = new FileInfo(file); + var rPath = Path.GetRelativePath(CopyOptions.Destination, file); + var pfi = new ProcessedFileInfo(rPath, FileClassType.NewDir, + fileClass: Configuration.LogParsing_ExtraDir, size: -1); + // Mark as extra/purged in results + resultsBuilder.AddFilePurged(pfi); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(pfi)); + } + + // Recurse into subdirectories of this extra dir + foreach (var subDir in Directory.EnumerateDirectories(destDir, "*", SearchOption.TopDirectoryOnly)) + { + cancellationToken.ThrowIfCancellationRequested(); + var extraInfo = new ProcessedFileInfo(subDir, FileClassType.NewDir, + fileClass: Configuration.LogParsing_ExtraDir, size: -1); + resultsBuilder.AddDir(extraInfo); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(extraInfo)); + PurgeExtraDirectory(subDir, resultsBuilder, cancellationToken); + } + + // Now delete the whole tree + try + { + Directory.Delete(destDir, true); + } + catch (Exception e) + { + OnCommandError?.Invoke(this, new CommandErrorEventArgs($"Unable to purge directory: {destDir}", e)); + } +} - } /// /// Yields the root pair and (if recurse is true) all sub-directory pairs, mirroring Robocopy's directory tree walk. From 951a3420f97215fc46f2dc8d4e31473f87f37413 Mon Sep 17 00:00:00 2001 From: Robert Brenckman <20431767+RFBomb@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:14:25 -0400 Subject: [PATCH 15/16] Create CommandTests.cs --- RoboSharpUnitTesting/CommandTests.cs | 721 +++++++++++++++++++++++++++ 1 file changed, 721 insertions(+) create mode 100644 RoboSharpUnitTesting/CommandTests.cs diff --git a/RoboSharpUnitTesting/CommandTests.cs b/RoboSharpUnitTesting/CommandTests.cs new file mode 100644 index 00000000..8b1bcb57 --- /dev/null +++ b/RoboSharpUnitTesting/CommandTests.cs @@ -0,0 +1,721 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RoboSharp; +using RoboSharp.Extensions; +using RoboSharp.Extensions.Tests; +using RoboSharp.Interfaces; +using RoboSharp.Results; +using RoboSharp.UnitTests; +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +#if NET6_0_OR_GREATER + +namespace RoboSharp.Extensions.Tests +{ + // ══════════════════════════════════════════════════════════════════════════ + // TEST FILE TREE (TEST_FILES/STANDARD — 4 files per folder, 3 levels) + // + // Root/ <- Source root (READ ONLY — never modified) + // 0_Bytes.txt + // 4_Bytes.txt + // 1024_Bytes.txt + // 65536_Bytes.txt + // SubFolder_1/ + // 0_Bytes.txt 4_Bytes.txt 1024_Bytes.txt 65536_Bytes.txt + // SubFolder_1.1/ + // 0_Bytes.txt 4_Bytes.txt 1024_Bytes.txt 65536_Bytes.txt + // SubFolder_1.2/ <- empty — exists only for /E tests + // SubFolder_2/ + // 0_Bytes.txt 4_Bytes.txt 1024_Bytes.txt 65536_Bytes.txt + // + // Counts (used in DataRow expectations below): + // Dirs (root + 4 subs + 1 empty) = 6 total source dirs + // Files 4 per non-empty dir × 5 = 20 files + // SubFolder_1.2 is empty → only appears in /E counts + // ══════════════════════════════════════════════════════════════════════════ + + /// + /// Known counts derived from the static TEST_FILES/STANDARD tree. + /// Update these constants if the test file set ever changes. + /// + internal static class SourceTree + { + // Non-empty dirs that contain files (not counting root) + public const int SubDirsWithFiles = 4; // SubFolder_1, SubFolder_1.1, SubFolder_1.2 area, SubFolder_2 + public const int EmptySubDirs = 1; // SubFolder_1.2 + public const int TotalSubDirs = SubDirsWithFiles + EmptySubDirs; // 5 + public const int TotalDirsWithRoot = TotalSubDirs + 1; // 6 + + public const int FilesPerDir = 4; + public const int DirsWithFiles = SubDirsWithFiles + 1; // +1 for root = 5 + public const int TotalFiles = DirsWithFiles * FilesPerDir; // 20 + } + + [TestClass] + public class RoboCommandTests : CommandTests { } + + // ══════════════════════════════════════════════════════════════════════════ + // + // CommandTests + // + // Abstract base that every IRoboCommand implementation test class inherits. + // Design principles: + // • No back-to-back RoboCopy runs — expected counts are compile-time constants. + // • Each test creates its own isolated temp destination (or move-source). + // • Source (TEST_FILES/STANDARD) is NEVER modified. + // • GetCommand is virtual — subclasses override to supply their T instance. + // + // ══════════════════════════════════════════════════════════════════════════ + [TestClass] + public abstract class CommandTests where T : IRoboCommand, new() + { + // ── MSTest plumbing ─────────────────────────────────────────────────── + + public TestContext TestContext { get; set; } = null!; + + /// Cooperative cancellation token wired to the MSTest timeout. + protected CancellationToken Token => TestContext.CancellationTokenSource.Token; + + // ── Directories ─────────────────────────────────────────────────────── + + /// Shared read-only source. Never modified by any test. + protected static string SharedSource => Test_Setup.Source_Standard; + + /// + /// Per-test isolated destination directory. + /// Created fresh in and deleted in . + /// + protected string TempDest { get; private set; } = string.Empty; + + [TestInitialize] + public void TestInit() + { + TempDest = Path.Combine( + Path.GetTempPath(), + "RoboSharp_CmdTests", + typeof(T).Name, + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(TempDest); + } + + [TestCleanup] + public void TestCleanup() + { + try + { + if (Directory.Exists(TempDest)) + { + // Clear read-only attributes before deleting (robocopy may set them) + foreach (var f in new DirectoryInfo(TempDest).GetFiles("*", SearchOption.AllDirectories)) + File.SetAttributes(f.FullName, FileAttributes.Normal); + Directory.Delete(TempDest, recursive: true); + } + } + catch { /* best-effort — don't fail the test on cleanup */ } + } + + // ── Command factory ─────────────────────────────────────────────────── + + /// + /// Creates an instance of and sets Source/Destination. + /// Subclasses override to inject factories, authenticators, or other dependencies. + /// The base implementation uses and + /// wires Source + Destination on . + /// + protected virtual T GetCommand(string source, string destination) + { + var cmd = Activator.CreateInstance(); + cmd.CopyOptions.Source = source; + cmd.CopyOptions.Destination = destination; + return cmd; + } + + // ── Run helper ──────────────────────────────────────────────────────── + + /// + /// Starts the command, wires cooperative cancellation, and returns results. + /// Swallows caused by the test timeout + /// so the test framework can report it as a timeout rather than an error. + /// + protected async Task RunCommand(T cmd) + { + Token.Register(() => cmd.Stop()); + try + { + return await cmd.StartAsync().WaitAsync(Token); + } + catch (OperationCanceledException) when (Token.IsCancellationRequested) + { + return null; // timeout — MSTest will report [Timeout] failure + } + } + + // ── Move-source helper ──────────────────────────────────────────────── + + /// + /// Copies the standard source tree into a fresh temp directory so that + /// move operations have their own expendable copy to consume. + /// + protected async Task PrepMoveSource() + { + string moveSource = Path.Combine( + Path.GetTempPath(), + "RoboSharp_MoveSource", + typeof(T).Name, + Guid.NewGuid().ToString("N")); + + Directory.CreateDirectory(moveSource); + + // Use a real RoboCommand to clone the source tree — read-only, no side-effects + var rc = new RoboCommand(); + rc.CopyOptions.Source = SharedSource; + rc.CopyOptions.Destination = moveSource; + rc.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + Token.Register(() => rc.Stop()); + await rc.StartAsync().WaitAsync(Token); + return moveSource; + } + + // ── Assert helper ───────────────────────────────────────────────────── + + /// + /// Compares a results object against pre-computed expected statistics, + /// printing both to the test output before asserting. + /// + protected static void AssertResults( + RoboCopyResults? results, + string label, + long expectedDirTotal, long expectedDirCopied, long expectedDirExtras, long expectedDirSkipped, + long expectedFileTotal, long expectedFileCopied, long expectedFileExtras, long expectedFileSkipped) + { + Assert.IsNotNull(results, "Results must not be null — command may have been cancelled by timeout."); + + // Print for diagnostics + Console.WriteLine($"── {label} ──"); + Console.WriteLine($" Dirs : {results.DirectoriesStatistic}"); + Console.WriteLine($" Files : {results.FilesStatistic}"); + Console.WriteLine($" Bytes : {results.BytesStatistic}"); + + Assert.AreEqual(expectedDirTotal, results.DirectoriesStatistic.Total, $"[{label}] Dir.Total"); + Assert.AreEqual(expectedDirCopied, results.DirectoriesStatistic.Copied, $"[{label}] Dir.Copied"); + Assert.AreEqual(expectedDirExtras, results.DirectoriesStatistic.Extras, $"[{label}] Dir.Extras"); + Assert.AreEqual(expectedDirSkipped, results.DirectoriesStatistic.Skipped, $"[{label}] Dir.Skipped"); + + Assert.AreEqual(expectedFileTotal, results.FilesStatistic.Total, $"[{label}] File.Total"); + Assert.AreEqual(expectedFileCopied, results.FilesStatistic.Copied, $"[{label}] File.Copied"); + Assert.AreEqual(expectedFileExtras, results.FilesStatistic.Extras, $"[{label}] File.Extras"); + Assert.AreEqual(expectedFileSkipped, results.FilesStatistic.Skipped, $"[{label}] File.Skipped"); + } + + + // ════════════════════════════════════════════════════════════════════════ + // COPY TESTS + // ════════════════════════════════════════════════════════════════════════ + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + public async Task Copy_Flat() + { + // Only root-level files copied; subdirs not traversed. + // Dir: total=1 (root), copied=1, extras=0, skipped=0 + // File: total=4, copied=4, extras=0, skipped=0 + var cmd = GetCommand(SharedSource, TempDest); + var results = await RunCommand(cmd); + + AssertResults(results, nameof(Copy_Flat), + expectedDirTotal: 1, expectedDirCopied: 1, expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.FilesPerDir, expectedFileCopied: SourceTree.FilesPerDir, + expectedFileExtras: 0, expectedFileSkipped: 0); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + public async Task Copy_Subdirectories() + { + // /S — recurse but skip empty dirs. + // Empty SubFolder_1.2 is not included in count. + // Dirs: root + SubDirsWithFiles = 1+4 = 5 + // Files: 5 dirs × 4 files = 20 + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectories = true; + var results = await RunCommand(cmd); + + AssertResults(results, nameof(Copy_Subdirectories), + expectedDirTotal: 1 + SourceTree.SubDirsWithFiles, + expectedDirCopied: 1 + SourceTree.SubDirsWithFiles, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles, + expectedFileCopied: SourceTree.TotalFiles, + expectedFileExtras: 0, expectedFileSkipped: 0); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + public async Task Copy_SubdirectoriesIncludingEmpty() + { + // /E — recurse including empty dirs. + // Dirs: root + TotalSubDirs = 1+5 = 6 + // Files: same 20 (empty dir has no files) + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + var results = await RunCommand(cmd); + + AssertResults(results, nameof(Copy_SubdirectoriesIncludingEmpty), + expectedDirTotal: SourceTree.TotalDirsWithRoot, + expectedDirCopied: SourceTree.TotalDirsWithRoot, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles, + expectedFileCopied: SourceTree.TotalFiles, + expectedFileExtras: 0, expectedFileSkipped: 0); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + [DataRow(1, DisplayName = "Depth=1 (root only)")] + [DataRow(2, DisplayName = "Depth=2 (root + 1 level)")] + public async Task Copy_WithDepthLimit(int depth) + { + // Depth=1: root dir only, 4 files. + // Depth=2: root + SubFolder_1 + SubFolder_2 = 3 dirs, 4+4+4=12 files. + // (SubFolder_1.1 is deeper than depth 2) + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + cmd.CopyOptions.Depth = depth; + var results = await RunCommand(cmd); + + long expectedDirs = depth == 1 ? 1 : 3; // root; root+Sub1+Sub2 + long expectedFiles = depth == 1 ? 4 : 12; + + AssertResults(results, $"Depth={depth}", + expectedDirTotal: expectedDirs, expectedDirCopied: expectedDirs, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: expectedFiles, expectedFileCopied: expectedFiles, + expectedFileExtras: 0, expectedFileSkipped: 0); + } + + // ════════════════════════════════════════════════════════════════════════ + // SKIP TESTS (destination already up to date) + // ════════════════════════════════════════════════════════════════════════ + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + public async Task Copy_SkipsAlreadyCopiedFiles() + { + // Run twice. Second run: all files exist in dest → all skipped. + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + await RunCommand(cmd); // first pass — populate dest + + // Second pass — same command, dest already populated + var cmd2 = GetCommand(SharedSource, TempDest); + cmd2.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + var results = await RunCommand(cmd2); + + AssertResults(results, nameof(Copy_SkipsAlreadyCopiedFiles), + expectedDirTotal: SourceTree.TotalDirsWithRoot, + expectedDirCopied: 0, expectedDirExtras: 0, + expectedDirSkipped: SourceTree.TotalDirsWithRoot, + expectedFileTotal: SourceTree.TotalFiles, + expectedFileCopied: 0, expectedFileExtras: 0, + expectedFileSkipped: SourceTree.TotalFiles); + } + + // ════════════════════════════════════════════════════════════════════════ + // EXTRA FILE / DIR REPORTING + // ════════════════════════════════════════════════════════════════════════ + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + [DataRow(1, DisplayName = "1 extra file in dest root")] + [DataRow(3, DisplayName = "3 extra files in dest root")] + public async Task ExtraFiles_AreReported(int extraFileCount) + { + // Pre-place extra files in dest root. + for (int i = 0; i < extraFileCount; i++) + File.WriteAllText(Path.Combine(TempDest, $"extra_{i}.txt"), "extra"); + + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + cmd.LoggingOptions.ReportExtraFiles = true; + var results = await RunCommand(cmd); + + // Files: 20 source copied + N extras in dest + AssertResults(results, $"{nameof(ExtraFiles_AreReported)}(n={extraFileCount})", + expectedDirTotal: SourceTree.TotalDirsWithRoot, + expectedDirCopied: SourceTree.TotalDirsWithRoot, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles + extraFileCount, + expectedFileCopied: SourceTree.TotalFiles, + expectedFileExtras: extraFileCount, + expectedFileSkipped: 0); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + [DataRow(1, DisplayName = "1 extra dir in dest root")] + [DataRow(3, DisplayName = "3 extra dirs in dest root")] + public async Task ExtraDirs_AreReported(int extraDirCount) + { + // Pre-place extra dirs (empty) in dest root. + for (int i = 0; i < extraDirCount; i++) + Directory.CreateDirectory(Path.Combine(TempDest, $"ExtraDir_{i}")); + + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + var results = await RunCommand(cmd); + + // Extra dirs appear in Extras column, not Copied. + // Total dirs = source dirs + extra dest dirs. + AssertResults(results, $"{nameof(ExtraDirs_AreReported)}(n={extraDirCount})", + expectedDirTotal: SourceTree.TotalDirsWithRoot + extraDirCount, + expectedDirCopied: SourceTree.TotalDirsWithRoot, + expectedDirExtras: extraDirCount, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles, + expectedFileCopied: SourceTree.TotalFiles, + expectedFileExtras: 0, expectedFileSkipped: 0); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + [DataRow(false, DisplayName = "Flat — extra dirs still reported")] + [DataRow(true, DisplayName = "Recursive — extra dirs reported")] + public async Task ExtraDirs_AreReported_RegardlessOfRecursionMode(bool recursive) + { + // RoboCopy always reports extra dirs in Extras regardless of /S or not. + Directory.CreateDirectory(Path.Combine(TempDest, "ExtraDir")); + + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectories = recursive; + var results = await RunCommand(cmd); + + // Extra dir count is always 1 regardless of recursion mode. + Assert.IsNotNull(results); + Assert.AreEqual(1L, results.DirectoriesStatistic.Extras, + "Extra dir must be reported in Extras regardless of recursion mode"); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + public async Task ExtraDirs_NotReported_WhenExcludeExtraSet() + { + Directory.CreateDirectory(Path.Combine(TempDest, "ExtraDir")); + + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + cmd.SelectionOptions.ExcludeExtra = true; + var results = await RunCommand(cmd); + + Assert.IsNotNull(results); + Assert.AreEqual(0L, results.DirectoriesStatistic.Extras, + "/XX (ExcludeExtra) must suppress extra dir reporting"); + } + + // ════════════════════════════════════════════════════════════════════════ + // PURGE TESTS + // ════════════════════════════════════════════════════════════════════════ + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + [DataRow(false, DisplayName = "Purge via /PURGE flag")] + [DataRow(true, DisplayName = "Purge via /MIR flag")] + public async Task Purge_ExtraFilesAreDeletedAndCounted(bool useMirror) + { + // 2 extra files sit in dest root before the run. + const int extraFiles = 2; + for (int i = 0; i < extraFiles; i++) + File.WriteAllText(Path.Combine(TempDest, $"purge_{i}.txt"), "delete me"); + + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + if (useMirror) cmd.CopyOptions.Mirror = true; + else cmd.CopyOptions.Purge = true; + var results = await RunCommand(cmd); + + // Extra files are counted in Extras then deleted. + AssertResults(results, $"Purge(mirror={useMirror})", + expectedDirTotal: SourceTree.TotalDirsWithRoot, + expectedDirCopied: SourceTree.TotalDirsWithRoot, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles + extraFiles, + expectedFileCopied: SourceTree.TotalFiles, + expectedFileExtras: extraFiles, + expectedFileSkipped: 0); + + // Physical verification — files must be gone + for (int i = 0; i < extraFiles; i++) + Assert.IsFalse(File.Exists(Path.Combine(TempDest, $"purge_{i}.txt")), + $"purge_{i}.txt should have been deleted"); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + [DataRow(1, 2, DisplayName = "1 extra dir, 2 files inside")] + [DataRow(2, 3, DisplayName = "2 extra dirs, 3 files each")] + public async Task Purge_ExtraDirsAndTheirFilesAreDeletedAndCounted( + int extraDirCount, int filesPerExtraDir) + { + // Pre-place extra dest dirs, each containing files. + for (int d = 0; d < extraDirCount; d++) + { + var dir = Path.Combine(TempDest, $"PurgeDir_{d}"); + Directory.CreateDirectory(dir); + for (int f = 0; f < filesPerExtraDir; f++) + File.WriteAllText(Path.Combine(dir, $"file_{f}.txt"), "purge"); + } + + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.Mirror = true; // /MIR = /E + /PURGE + + var results = await RunCommand(cmd); + + long expectedPurgedFiles = extraDirCount * filesPerExtraDir; + + AssertResults(results, + $"Purge dirs(dirs={extraDirCount}, files={filesPerExtraDir})", + expectedDirTotal: SourceTree.TotalDirsWithRoot + extraDirCount, + expectedDirCopied: SourceTree.TotalDirsWithRoot, + expectedDirExtras: extraDirCount, + expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles + expectedPurgedFiles, + expectedFileCopied: SourceTree.TotalFiles, + expectedFileExtras: expectedPurgedFiles, + expectedFileSkipped: 0); + + // Physical verification + for (int d = 0; d < extraDirCount; d++) + Assert.IsFalse(Directory.Exists(Path.Combine(TempDest, $"PurgeDir_{d}")), + $"PurgeDir_{d} should have been deleted"); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + [DataRow(2, DisplayName = "2-level nested extra dir")] + [DataRow(3, DisplayName = "3-level nested extra dir")] + public async Task Purge_NestedExtraDirsCountAllFiles(int nestDepth) + { + // Build a chain: dest/nested0/nested1/... each level has 1 file. + string current = TempDest; + for (int depth = 0; depth < nestDepth; depth++) + { + current = Path.Combine(current, $"nested_{depth}"); + Directory.CreateDirectory(current); + File.WriteAllText(Path.Combine(current, $"file_{depth}.txt"), "purge"); + } + + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.Mirror = true; + var results = await RunCommand(cmd); + + // nestDepth dirs + nestDepth files inside them should all be counted + AssertResults(results, $"NestedPurge(depth={nestDepth})", + expectedDirTotal: SourceTree.TotalDirsWithRoot + nestDepth, + expectedDirCopied: SourceTree.TotalDirsWithRoot, + expectedDirExtras: nestDepth, + expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles + nestDepth, + expectedFileCopied: SourceTree.TotalFiles, + expectedFileExtras: nestDepth, + expectedFileSkipped: 0); + + Assert.IsFalse(Directory.Exists(Path.Combine(TempDest, "nested_0")), + "Root of nested extra dir tree should have been purged"); + } + + // ════════════════════════════════════════════════════════════════════════ + // LIST-ONLY + // ════════════════════════════════════════════════════════════════════════ + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + [DataRow(false, DisplayName = "ListOnly flat")] + [DataRow(true, DisplayName = "ListOnly recursive")] + public async Task ListOnly_ReportsWithoutWriting(bool recursive) + { + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = recursive; + cmd.LoggingOptions.ListOnly = true; + var results = await RunCommand(cmd); + + long expectedDirs = recursive ? SourceTree.TotalDirsWithRoot : 1; + long expectedFiles = recursive ? SourceTree.TotalFiles : SourceTree.FilesPerDir; + + AssertResults(results, $"ListOnly(recursive={recursive})", + expectedDirTotal: expectedDirs, expectedDirCopied: expectedDirs, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: expectedFiles, expectedFileCopied: expectedFiles, + expectedFileExtras: 0, expectedFileSkipped: 0); + + // Nothing should have been written to disk + var written = Directory.GetFiles(TempDest, "*", SearchOption.AllDirectories); + Assert.AreEqual(0, written.Length, "ListOnly must not write any files to destination"); + } + + // ════════════════════════════════════════════════════════════════════════ + // MOVE TESTS + // Each move test calls PrepMoveSource() to get an expendable copy. + // ════════════════════════════════════════════════════════════════════════ + + [TestMethod, Timeout(10000, CooperativeCancellation = true)] + public async Task Move_Files_FlatOnly() + { + string moveSource = await PrepMoveSource(); + try + { + var cmd = GetCommand(moveSource, TempDest); + cmd.CopyOptions.MoveFiles = true; + var results = await RunCommand(cmd); + + // Files moved from root only; dirs remain in source + AssertResults(results, nameof(Move_Files_FlatOnly), + expectedDirTotal: 1, expectedDirCopied: 1, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.FilesPerDir, + expectedFileCopied: SourceTree.FilesPerDir, + expectedFileExtras: 0, expectedFileSkipped: 0); + + // Source root files should be gone; subdirs untouched + Assert.AreEqual(0, Directory.GetFiles(moveSource, "*", SearchOption.TopDirectoryOnly).Length, + "Source root files should have been moved"); + Assert.IsTrue(Directory.GetDirectories(moveSource).Length > 0, + "/MOV should not delete source subdirs"); + } + finally + { + try { Directory.Delete(moveSource, true); } catch { } + } + } + + [TestMethod, Timeout(10000, CooperativeCancellation = true)] + public async Task Move_FilesAndDirectories_Recursive() + { + string moveSource = await PrepMoveSource(); + try + { + var cmd = GetCommand(moveSource, TempDest); + cmd.CopyOptions.MoveFilesAndDirectories = true; + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + var results = await RunCommand(cmd); + + AssertResults(results, nameof(Move_FilesAndDirectories_Recursive), + expectedDirTotal: SourceTree.TotalDirsWithRoot, + expectedDirCopied: SourceTree.TotalDirsWithRoot, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles, + expectedFileCopied: SourceTree.TotalFiles, + expectedFileExtras: 0, expectedFileSkipped: 0); + + // Source should be completely empty after /MOVE + var remaining = Directory.GetFileSystemEntries(moveSource, "*", SearchOption.AllDirectories); + Assert.AreEqual(0, remaining.Length, + "/MOVE should leave source directory empty"); + } + finally + { + try { Directory.Delete(moveSource, true); } catch { } + } + } + + // ════════════════════════════════════════════════════════════════════════ + // FILE FILTER / EXCLUSION + // ════════════════════════════════════════════════════════════════════════ + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + public async Task FileFilter_LimitsFilesCopied() + { + // Only *.txt files — excludes 4_Bytes.htm files if present. + // In the standard tree all 4 files per dir are .txt, so count stays 20. + // This test validates the filter is applied, not that it excludes anything — + // override in subclasses if the file set has mixed extensions. + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + cmd.CopyOptions.FileFilter = new[] { "*.txt" }; + var results = await RunCommand(cmd); + + Assert.IsNotNull(results); + // All files matching *.txt should be counted; non-matching skipped by filter + Assert.AreEqual(0L, results.FilesStatistic.Failed, "No files should fail"); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + public async Task FileExclusion_ExcludesMatchingFiles() + { + // Exclude files matching "*0*_Bytes*" (hits 0_Bytes.txt in each dir) + // Each of the 5 dirs-with-files has 1 matching file → 5 excluded, 15 copied + const int excludedPerDir = 1; + long expectedCopied = SourceTree.TotalFiles - (SourceTree.DirsWithFiles * excludedPerDir); + long expectedSkipped = SourceTree.DirsWithFiles * excludedPerDir; + + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + cmd.SelectionOptions.ExcludedFiles.Add("*0*_Bytes*"); + var results = await RunCommand(cmd); + + AssertResults(results, nameof(FileExclusion_ExcludesMatchingFiles), + expectedDirTotal: SourceTree.TotalDirsWithRoot, + expectedDirCopied: SourceTree.TotalDirsWithRoot, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles, + expectedFileCopied: expectedCopied, + expectedFileExtras: 0, + expectedFileSkipped: expectedSkipped); + } + + [TestMethod, Timeout(5000, CooperativeCancellation = true)] + public async Task DirectoryExclusion_ExcludesMatchingDirs() + { + // Exclude SubFolder_2 → loses 1 dir + 4 files + const int excludedDirs = 1; + const int excludedFiles = excludedDirs * SourceTree.FilesPerDir; + + var cmd = GetCommand(SharedSource, TempDest); + cmd.CopyOptions.CopySubdirectoriesIncludingEmpty = true; + cmd.SelectionOptions.ExcludedDirectories.Add("SubFolder_2"); + var results = await RunCommand(cmd); + + AssertResults(results, nameof(DirectoryExclusion_ExcludesMatchingDirs), + expectedDirTotal: SourceTree.TotalDirsWithRoot - excludedDirs, + expectedDirCopied: SourceTree.TotalDirsWithRoot - excludedDirs, + expectedDirExtras: 0, expectedDirSkipped: 0, + expectedFileTotal: SourceTree.TotalFiles - excludedFiles, + expectedFileCopied: SourceTree.TotalFiles - excludedFiles, + expectedFileExtras: 0, expectedFileSkipped: 0); + } + } + + + // ══════════════════════════════════════════════════════════════════════════ + // + // RoboCommand_Tests + // + // Runs the full CommandTests suite against the real RoboCopy process. + // If these tests pass, the expected counts in CommandTests are correct. + // If they fail, fix the SourceTree constants or test expectations first + // before debugging any custom implementation. + // + // ══════════════════════════════════════════════════════════════════════════ + [TestClass] + public class RoboCommand_Tests : CommandTests + { + protected override RoboCommand GetCommand(string source, string destination) + { + var cmd = new RoboCommand(); + cmd.CopyOptions.Source = source; + cmd.CopyOptions.Destination = destination; + return cmd; + } + } + + + // ══════════════════════════════════════════════════════════════════════════ + // + // RoboCommandPortable_CommandTests + // + // Runs the full CommandTests suite against RoboCommandPortable. + // Failures here indicate bugs in the portable implementation, not in + // the test expectations (which are validated by RoboCommand_Tests above). + // + // ══════════════════════════════════════════════════════════════════════════ + [TestClass] + public class RoboCommandPortable_CommandTests : CommandTests + { + protected override RoboCommandPortable GetCommand(string source, string destination) + { + var cmd = new RoboCommandPortable(StreamedCopierFactory.DefaultFactory); + cmd.CopyOptions.Source = source; + cmd.CopyOptions.Destination = destination; + return cmd; + } + } +} + +#endif \ No newline at end of file From 2a20b5f317e963bcf313235e0fe1532d8eabc0fc Mon Sep 17 00:00:00 2001 From: Robert Brenckman <20431767+RFBomb@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:25:53 -0400 Subject: [PATCH 16/16] Update CommandTests.cs --- RoboSharpUnitTesting/CommandTests.cs | 52 +++++++++++++--------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/RoboSharpUnitTesting/CommandTests.cs b/RoboSharpUnitTesting/CommandTests.cs index 8b1bcb57..2bbff36d 100644 --- a/RoboSharpUnitTesting/CommandTests.cs +++ b/RoboSharpUnitTesting/CommandTests.cs @@ -1,7 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using RoboSharp; -using RoboSharp.Extensions; -using RoboSharp.Extensions.Tests; using RoboSharp.Interfaces; using RoboSharp.Results; using RoboSharp.UnitTests; @@ -54,9 +52,28 @@ internal static class SourceTree public const int TotalFiles = DirsWithFiles * FilesPerDir; // 20 } +// ══════════════════════════════════════════════════════════════════════════ + // + // RoboCommand_Tests + // + // Runs the full CommandTests suite against the real RoboCopy process. + // If these tests pass, the expected counts in CommandTests are correct. + // If they fail, fix the SourceTree constants or test expectations first + // before debugging any custom implementation. + // + // ══════════════════════════════════════════════════════════════════════════ [TestClass] - public class RoboCommandTests : CommandTests { } - + public class RoboCommand_Tests : CommandTests + { + protected override RoboCommand GetCommand(string source, string destination) + { + var cmd = new RoboCommand(); + cmd.CopyOptions.Source = source; + cmd.CopyOptions.Destination = destination; + return cmd; + } + } + // ══════════════════════════════════════════════════════════════════════════ // // CommandTests @@ -671,31 +688,9 @@ public async Task DirectoryExclusion_ExcludesMatchingDirs() expectedFileExtras: 0, expectedFileSkipped: 0); } } + - - // ══════════════════════════════════════════════════════════════════════════ - // - // RoboCommand_Tests - // - // Runs the full CommandTests suite against the real RoboCopy process. - // If these tests pass, the expected counts in CommandTests are correct. - // If they fail, fix the SourceTree constants or test expectations first - // before debugging any custom implementation. - // - // ══════════════════════════════════════════════════════════════════════════ - [TestClass] - public class RoboCommand_Tests : CommandTests - { - protected override RoboCommand GetCommand(string source, string destination) - { - var cmd = new RoboCommand(); - cmd.CopyOptions.Source = source; - cmd.CopyOptions.Destination = destination; - return cmd; - } - } - - +/* // ══════════════════════════════════════════════════════════════════════════ // // RoboCommandPortable_CommandTests @@ -716,6 +711,7 @@ protected override RoboCommandPortable GetCommand(string source, string destinat return cmd; } } ++*/ } #endif \ No newline at end of file