diff --git a/Directory.Build.props b/Directory.Build.props
index d0039ca..f4e293e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,7 +11,7 @@
True
True
True
- 2.2.2
+ 2.3.0
Kotz
Copyright © Kotz 2022
https://github.com/Kaoticz/Kotz.Utilities
diff --git a/Kotz.Extensions/KotzUtilities.cs b/Kotz.Extensions/KotzUtilities.cs
new file mode 100644
index 0000000..adb8710
--- /dev/null
+++ b/Kotz.Extensions/KotzUtilities.cs
@@ -0,0 +1,404 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Kotz.Extensions;
+
+///
+/// A collection of utility methods.
+///
+public static class KotzUtilities
+{
+ private static readonly string _programVerifier = (OperatingSystem.IsWindows()) ? "where" : "which";
+ private static readonly string _envPathSeparator = (OperatingSystem.IsWindows()) ? ";" : ":";
+ private static readonly EnvironmentVariableTarget _envTarget = (OperatingSystem.IsWindows())
+ ? EnvironmentVariableTarget.User
+ : EnvironmentVariableTarget.Process;
+
+ ///
+ /// Adds a directory path to the PATH environment variable.
+ ///
+ /// The absolute path to a directory.
+ ///
+ /// On Windows, this method needs to be called once and the dependencies will be available for the user forever.
+ /// On Unix systems, it's only possible to add to the PATH on a process basis, so this method needs to be called
+ /// at least once everytime the application is executed.
+ ///
+ /// if got successfully added to the PATH envar, otherwise.
+ ///
+ ///
+ public static bool AddPathToPATHEnvar(string directoryUri)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(directoryUri, nameof(directoryUri));
+
+ if (File.Exists(directoryUri))
+ throw new ArgumentException("Parameter must point to a directory, not a file.", nameof(directoryUri));
+
+ var envPathValue = Environment.GetEnvironmentVariable("PATH", _envTarget) ?? string.Empty;
+
+ // If directoryPath is already in the PATH envar, don't add it again.
+ if (envPathValue.Contains(directoryUri, StringComparison.Ordinal))
+ return false;
+
+ var newPathEnvValue = envPathValue + _envPathSeparator + directoryUri;
+
+ // Add path to Windows' user envar, so it persists across reboots.
+ if (OperatingSystem.IsWindows())
+ Environment.SetEnvironmentVariable("PATH", newPathEnvValue, EnvironmentVariableTarget.User);
+
+ // Add path to the current process' envar, so the program can see the dependencies.
+ Environment.SetEnvironmentVariable("PATH", newPathEnvValue, EnvironmentVariableTarget.Process);
+
+ return true;
+ }
+
+ ///
+ /// Checks if this application can write to the specified directory.
+ ///
+ /// The absolute path to a directory.
+ /// if writing is allowed, otherwise.
+ ///
+ ///
+ public static bool HasWritePermissionAt(ReadOnlySpan directoryUri)
+ {
+ var tempFileUri = Path.Join(directoryUri, Path.GetRandomFileName() + ".tmp");
+
+ try
+ {
+ using var fileStream = File.Create(tempFileUri);
+ return true;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return false;
+ }
+ finally
+ {
+ TryDeleteFile(tempFileUri);
+ }
+ }
+
+
+ ///
+ /// Checks if a program exists at the specified absolute path or the PATH environment variable.
+ ///
+ /// The name of the program.
+ /// if the program exists, otherwise.
+ ///
+ ///
+ public static bool ProgramExists(string programName)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(programName, nameof(programName));
+
+ using var process = StartProcess(_programVerifier, programName, false, false);
+ process.WaitForExit();
+
+ return process.ExitCode is 0;
+ }
+
+
+ ///
+ /// Starts the specified program in the background.
+ ///
+ ///
+ /// The name of the program in the PATH environment variable,
+ /// or the absolute path to its executable.
+ ///
+ /// The arguments to the program.
+ /// Determines whether Standard Output should be redirected.
+ /// Determines whether Standard Error should be redirected.
+ ///
+ /// The parameter is not escaped, you can either escape it yourself or
+ /// use instead.
+ ///
+ /// The process of the specified program.
+ ///
+ ///
+ /// Occurs when does not exist.
+ /// Occurs when the process fails to execute.
+ public static Process StartProcess(string program, string arguments = "", bool redirectStdout = true, bool redirectStderr = true)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(program, nameof(program));
+ ArgumentNullException.ThrowIfNull(arguments, nameof(arguments));
+
+ return Process.Start(new ProcessStartInfo()
+ {
+ FileName = program,
+ Arguments = arguments,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = redirectStdout,
+ RedirectStandardError = redirectStderr
+ }) ?? throw new InvalidOperationException($"Failed spawing process for: {program} {arguments}");
+ }
+
+
+ ///
+ /// Starts the specified program in the background.
+ ///
+ ///
+ /// The name of the program in the PATH environment variable,
+ /// or the absolute path to its executable.
+ ///
+ /// The arguments to the program.
+ /// Determines whether Standard Output should be redirected.
+ /// Determines whether Standard Error should be redirected.
+ /// The get automatically escaped.
+ /// The process of the specified program.
+ ///
+ ///
+ /// Occurs when does not exist.
+ /// Occurs when the process fails to execute.
+ public static Process StartProcess(string program, IEnumerable arguments, bool redirectStdout = true, bool redirectStderr = true)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(program, nameof(program));
+ ArgumentNullException.ThrowIfNull(arguments, nameof(arguments));
+
+ var processInfo = new ProcessStartInfo()
+ {
+ FileName = program,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = redirectStdout,
+ RedirectStandardError = redirectStderr
+ };
+
+ foreach (var argument in arguments)
+ processInfo.ArgumentList.Add(argument);
+
+ return Process.Start(processInfo)
+ ?? throw new InvalidOperationException($"Failed spawing process for: {program} {string.Join(' ', processInfo.ArgumentList)}");
+ }
+
+ ///
+ /// Safely creates a object with the specified method.
+ ///
+ /// The factory method that creates the object.
+ /// The created object or if creation does not succeed.
+ /// The exception thrown by the or if creation of the object succeeds.
+ /// The type of the object being created.
+ /// if the object was successfully created, otherwise.
+ public static bool TryCreate(Func factory, [MaybeNullWhen(false)] out T result, [MaybeNullWhen(true)] out Exception exception)
+ {
+ try
+ {
+ result = factory();
+ exception = default;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ result = default;
+ exception = ex;
+ return false;
+ }
+ }
+
+ ///
+ /// Safely deletes a file or directory.
+ ///
+ /// The absolute path to the File System Object.
+ ///
+ /// to remove directories, subdirectories, and files in the path, otherwise.
+ /// This is only used when deleting a directory.
+ ///
+ /// if the File System Object was deleted, otherwise.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static bool TryDeleteFSO(string fsoUri, bool isRecursive = true)
+ => TryDeleteFile(fsoUri) || TryDeleteDirectory(fsoUri, isRecursive);
+
+ ///
+ /// Safely deletes a file.
+ ///
+ /// The absolute path to the file.
+ /// if the file was deleted, otherwise.
+ ///
+ ///
+ public static bool TryDeleteFile(string fileUri)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(fileUri, nameof(fileUri));
+
+ if (!File.Exists(fileUri))
+ return false;
+
+ try
+ {
+ File.Delete(fileUri);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Safely deletes a directory.
+ ///
+ /// The absolute path to the directory.
+ ///
+ /// to remove directories, subdirectories, and files in the path, otherwise.
+ ///
+ /// if the directory was deleted, otherwise.
+ ///
+ ///
+ public static bool TryDeleteDirectory(string directoryUri, bool isRecursive = true)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(directoryUri, nameof(directoryUri));
+
+ if (!Directory.Exists(directoryUri))
+ return false;
+
+ try
+ {
+ Directory.Delete(directoryUri, isRecursive);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Safely moves a file or directory.
+ ///
+ /// The path to the source file or directory.
+ /// The path to the destination file or directory.
+ /// if the file system object got successfully moved, otherwise.
+ ///
+ ///
+ public static bool TryMoveFSO(string source, string destination)
+ => TryMoveFile(source, destination) || TryMoveDirectory(source, destination);
+
+ ///
+ /// Safely moves a file.
+ ///
+ /// The path to the source file.
+ /// The path to the destination file.
+ /// if the file got successfully moved, otherwise.
+ ///
+ ///
+ public static bool TryMoveFile(string source, string destination)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(source, nameof(source));
+ ArgumentException.ThrowIfNullOrEmpty(destination, nameof(destination));
+
+ if (!File.Exists(source))
+ return false;
+
+ try
+ {
+ var directoryUri = Directory.GetParent(destination)
+ ?? Directory.CreateDirectory(Directory.GetDirectoryRoot(destination));
+
+ if (!directoryUri.Exists)
+ directoryUri.Create();
+
+ File.Move(source, destination);
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Safely moves a directory.
+ ///
+ /// The path to the source directory.
+ /// The path to the destination directory.
+ /// if the directory got successfully moved, otherwise.
+ ///
+ ///
+ public static bool TryMoveDirectory(string source, string destination)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(source, nameof(source));
+ ArgumentException.ThrowIfNullOrEmpty(destination, nameof(destination));
+
+ if (!Directory.Exists(source))
+ return false;
+
+ try
+ {
+ var directoryUri = Directory.GetParent(destination)
+ ?? Directory.CreateDirectory(Directory.GetDirectoryRoot(destination));
+
+ if (!directoryUri.Exists)
+ directoryUri.Create();
+
+ Directory.Move(source, destination);
+ return true;
+ }
+ catch (IOException)
+ {
+ try
+ {
+ return CopyMoveDirectory(source, destination, true);
+ }
+ catch
+ {
+ TryDeleteDirectory(destination, true);
+ return false;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Moves a directory from to .
+ ///
+ /// The path to the source directory.
+ /// The path to the destination directory.
+ /// Determines whether the files and directories in should be deleted or not.
+ /// Use this method to circumvent this issue: https://github.com/dotnet/runtime/issues/31149
+ /// if the directory got moved, otherwise.
+ /// Occurs when one of the files to be moved is still in use.
+ ///
+ ///
+ private static bool CopyMoveDirectory(string source, string destination, bool deleteSourceFSOs)
+ {
+ if (!Directory.Exists(source) || !Uri.IsWellFormedUriString(destination, UriKind.RelativeOrAbsolute))
+ return false;
+
+ var sourceDir = Directory.CreateDirectory(source);
+ var destinationDir = (Directory.Exists(destination))
+ ? Directory.CreateDirectory(Path.Join(destination, Path.GetFileName(source)))
+ : Directory.CreateDirectory(destination);
+
+ foreach (var fileUri in Directory.EnumerateFiles(source))
+ File.Copy(fileUri, Path.Join(destinationDir.FullName, Path.GetFileName(fileUri)));
+
+ foreach (var directoryUri in Directory.EnumerateDirectories(source))
+ CopyMoveDirectory(directoryUri, Path.Join(destinationDir.FullName, Path.GetFileName(directoryUri)), false);
+
+ if (deleteSourceFSOs)
+ {
+ // If 'source' is not the root of the drive, delete it.
+ if (sourceDir.FullName != Path.GetPathRoot(source))
+ sourceDir.Delete(true);
+ else
+ {
+ // Else, delete all file system objects directly.
+ foreach (var fileUri in Directory.EnumerateFiles(source))
+ File.Delete(fileUri);
+
+ foreach (var directoryUri in Directory.EnumerateDirectories(source))
+ Directory.Delete(directoryUri, true);
+ }
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Kotz.Extensions/README.md b/Kotz.Extensions/README.md
index 4c6067c..3908307 100644
--- a/Kotz.Extensions/README.md
+++ b/Kotz.Extensions/README.md
@@ -78,4 +78,19 @@ Defines the following extension methods:
- ToTitleCase: Converts the current string to the *Title Case* format.
- **StringBuilder Extensions**
- ReplaceAll: Replaces all instances of a substring with another substring, even if the new substring is a substring of the old substring.
- - ToStringAndClear: Returns the `string` value of the current builder and clears the builder.
\ No newline at end of file
+ - ToStringAndClear: Returns the `string` value of the current builder and clears the builder.
+
+Defines the following types:
+
+- **KotzUtilities**: static class with a wide range of helper methods.
+ - AddPathToPATHEnvar: Adds a directory path to the PATH environment variable.
+ - HasWritePermissionAt: Checks if this application can write to the specified directory.
+ - ProgramExists: Checks if a program exists at the specified absolute path or the PATH environment variable.
+ - StartProcess: Starts the specified program in the background.
+ - TryCreate: Safely creates an object with the specified factory method.
+ - TryDeleteFSO: Safely deletes a file or directory.
+ - TryDeleteFile: Safely deletes a file.
+ - TryDeleteDirectory: Safely deletes a directory.
+ - TryMoveFSO: Safely moves a file or directory.
+ - TryMoveFile: Safely moves a file.
+ - TryMoveDirectory: Safely moves a directory.
\ No newline at end of file
diff --git a/Kotz.Tests/Events/EventAsyncTests.cs b/Kotz.Tests/Events/EventAsyncTests.cs
index 265b5fb..949c266 100644
--- a/Kotz.Tests/Events/EventAsyncTests.cs
+++ b/Kotz.Tests/Events/EventAsyncTests.cs
@@ -2,9 +2,9 @@
namespace Kotz.Tests.Events;
-public sealed class EventAyncTests
+public sealed class EventAsyncTests
{
- private readonly AsyncEvent _asyncEvent = new();
+ private readonly AsyncEvent _asyncEvent = new();
internal int Count { get; private set; }
diff --git a/Kotz.Tests/Extensions/Utilities/HasWritePermissionAtTests.cs b/Kotz.Tests/Extensions/Utilities/HasWritePermissionAtTests.cs
new file mode 100644
index 0000000..54b15db
--- /dev/null
+++ b/Kotz.Tests/Extensions/Utilities/HasWritePermissionAtTests.cs
@@ -0,0 +1,25 @@
+namespace Kotz.Tests.Extensions.Utilities;
+
+public sealed class HasWritePermissionAtTests
+{
+ [Fact]
+ internal void HasWritePermissionAtTrueTest()
+ => Assert.True(KotzUtilities.HasWritePermissionAt(AppContext.BaseDirectory));
+
+ [Fact]
+ internal void HasWritePermissionAtFalseTest()
+ {
+ var directoryUri = OperatingSystem.IsWindows()
+ ? Environment.GetFolderPath(Environment.SpecialFolder.System)
+ : "/";
+
+ Assert.False(KotzUtilities.HasWritePermissionAt(directoryUri));
+ }
+
+ [Fact]
+ internal void HasWritePermissionAtFailTest()
+ {
+ var fakeUri = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Personal), Guid.NewGuid().ToString());
+ Assert.Throws(() => KotzUtilities.HasWritePermissionAt(fakeUri));
+ }
+}
\ No newline at end of file
diff --git a/Kotz.Tests/Extensions/Utilities/ProgramExistsTests.cs b/Kotz.Tests/Extensions/Utilities/ProgramExistsTests.cs
new file mode 100644
index 0000000..48f47ef
--- /dev/null
+++ b/Kotz.Tests/Extensions/Utilities/ProgramExistsTests.cs
@@ -0,0 +1,16 @@
+namespace Kotz.Tests.Extensions.Utilities;
+
+public sealed class ProgramExistsTests
+{
+ [Theory]
+ [InlineData(true, "echo")]
+ [InlineData(false, "abcde")]
+ internal void ProgramExistsSuccessTest(bool expected, string command)
+ => Assert.Equal(expected, KotzUtilities.ProgramExists(command));
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ internal void ProgramExistsFailTest(string? command)
+ => Assert.ThrowsAny(() => KotzUtilities.ProgramExists(command!));
+}
\ No newline at end of file
diff --git a/Kotz.Tests/Extensions/Utilities/StartProgramTests.cs b/Kotz.Tests/Extensions/Utilities/StartProgramTests.cs
new file mode 100644
index 0000000..94f73e5
--- /dev/null
+++ b/Kotz.Tests/Extensions/Utilities/StartProgramTests.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel;
+
+namespace Kotz.Tests.Extensions.Utilities;
+
+public sealed class StartProgramTests
+{
+ [Fact]
+ internal void StartProgramSuccessTest()
+ {
+ using var process1 = KotzUtilities.StartProcess("echo", "Hello from xUnit!");
+ using var process2 = KotzUtilities.StartProcess("echo", ["Hello from xUnit!"]);
+
+ Assert.NotNull(process1);
+ Assert.NotNull(process2);
+ }
+
+ [Fact]
+ internal void StartProgramFailTest()
+ {
+ Assert.Throws(() => KotzUtilities.StartProcess("idonotexist"));
+ Assert.Throws(() => KotzUtilities.StartProcess("idonotexist", ["args"]));
+ Assert.Throws(() => KotzUtilities.StartProcess("", (string?)null!));
+ Assert.Throws(() => KotzUtilities.StartProcess("", Enumerable.Empty()));
+ Assert.Throws(() => KotzUtilities.StartProcess(null!, ""));
+ Assert.Throws(() => KotzUtilities.StartProcess(null!, Enumerable.Empty()));
+ Assert.Throws(() => KotzUtilities.StartProcess("idonotexist", (string?)null!));
+ Assert.Throws(() => KotzUtilities.StartProcess("idonotexist", (IEnumerable?)null!));
+ }
+}
\ No newline at end of file
diff --git a/Kotz.Tests/Extensions/Utilities/TryCreateTests.cs b/Kotz.Tests/Extensions/Utilities/TryCreateTests.cs
new file mode 100644
index 0000000..32804a9
--- /dev/null
+++ b/Kotz.Tests/Extensions/Utilities/TryCreateTests.cs
@@ -0,0 +1,22 @@
+using System.Text;
+
+namespace Kotz.Tests.Extensions.Utilities;
+
+public sealed class TryCreateTests
+{
+ [Fact]
+ internal void TryCreateSuccessTest()
+ {
+ Assert.True(KotzUtilities.TryCreate(() => new StringBuilder(), out var actualResult, out var exception));
+ Assert.IsType(actualResult);
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ internal void TryCreateFailTest()
+ {
+ Assert.False(KotzUtilities.TryCreate(() => throw new InvalidOperationException(), out var actualResult, out var exception));
+ Assert.IsType(exception);
+ Assert.Null(actualResult);
+ }
+}
\ No newline at end of file
diff --git a/Kotz.Tests/Extensions/Utilities/TryDeleteDirectoryTests.cs b/Kotz.Tests/Extensions/Utilities/TryDeleteDirectoryTests.cs
new file mode 100644
index 0000000..4a36d4d
--- /dev/null
+++ b/Kotz.Tests/Extensions/Utilities/TryDeleteDirectoryTests.cs
@@ -0,0 +1,40 @@
+namespace Kotz.Tests.Extensions.Utilities;
+
+public sealed class TryDeleteDirectoryTests
+{
+ [Theory]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ internal void TryDeleteDirectorySuccessTest(bool createDirectory, bool expected)
+ {
+ var directoryPath = CreateDirectoryPath(createDirectory);
+
+ Assert.Equal(createDirectory, Directory.Exists(directoryPath));
+ Assert.Equal(expected, KotzUtilities.TryDeleteDirectory(directoryPath));
+
+ if (createDirectory)
+ Assert.Equal(!createDirectory, Directory.Exists(directoryPath));
+ }
+
+ [Fact]
+ internal void TryDeleteDirectoryFailTest()
+ {
+ Assert.Throws(() => KotzUtilities.TryDeleteDirectory(string.Empty));
+ Assert.Throws(() => KotzUtilities.TryDeleteDirectory(null!));
+ }
+
+ ///
+ /// Creates a random directory path.
+ ///
+ /// if the directory should be created, otherwise.
+ /// The path to the directory.
+ internal static string CreateDirectoryPath(bool createDirectory)
+ {
+ var directoryPath = Path.Join(Path.GetTempPath(), Path.GetRandomFileName());
+
+ if (createDirectory)
+ Directory.CreateDirectory(directoryPath).Create();
+
+ return directoryPath;
+ }
+}
\ No newline at end of file
diff --git a/Kotz.Tests/Extensions/Utilities/TryDeleteFileTests.cs b/Kotz.Tests/Extensions/Utilities/TryDeleteFileTests.cs
new file mode 100644
index 0000000..6057542
--- /dev/null
+++ b/Kotz.Tests/Extensions/Utilities/TryDeleteFileTests.cs
@@ -0,0 +1,40 @@
+namespace Kotz.Tests.Extensions.Utilities;
+
+public sealed class TryDeleteFileTests
+{
+ [Theory]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ internal void TryDeleteFileSuccessTest(bool createFile, bool expected)
+ {
+ var filePath = CreateFilePath(createFile);
+
+ Assert.Equal(createFile, File.Exists(filePath));
+ Assert.Equal(expected, KotzUtilities.TryDeleteFile(filePath));
+
+ if (createFile)
+ Assert.Equal(!createFile, File.Exists(filePath));
+ }
+
+ [Fact]
+ internal void TryDeleteFileFailTest()
+ {
+ Assert.Throws(() => KotzUtilities.TryDeleteFile(string.Empty));
+ Assert.Throws(() => KotzUtilities.TryDeleteFile(null!));
+ }
+
+ ///
+ /// Creates a random file path.
+ ///
+ /// if the file should be created, otherwise.
+ /// The path to the file.
+ internal static string CreateFilePath(bool createFile)
+ {
+ var filePath = Path.Join(Path.GetTempPath(), Path.GetRandomFileName() + ".tmp");
+
+ if (createFile)
+ File.Create(filePath).Dispose();
+
+ return filePath;
+ }
+}
\ No newline at end of file
diff --git a/Kotz.Tests/Extensions/Utilities/TryMoveDirectoryTests.cs b/Kotz.Tests/Extensions/Utilities/TryMoveDirectoryTests.cs
new file mode 100644
index 0000000..16d567f
--- /dev/null
+++ b/Kotz.Tests/Extensions/Utilities/TryMoveDirectoryTests.cs
@@ -0,0 +1,86 @@
+namespace Kotz.Tests.Extensions.Utilities;
+
+public sealed class TryMoveDirectoryTests
+{
+ [Theory]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ internal void TryMoveDirectoryRenameSuccessTests(bool createdirectory, bool expected)
+ {
+ var oldPath = TryDeleteDirectoryTests.CreateDirectoryPath(createdirectory);
+ var newPath = TryDeleteDirectoryTests.CreateDirectoryPath(false);
+
+ Assert.Equal(createdirectory, Directory.Exists(oldPath));
+ Assert.Equal(expected, KotzUtilities.TryMoveDirectory(oldPath, newPath));
+
+ if (!createdirectory)
+ return;
+
+ Assert.Equal(!createdirectory, Directory.Exists(oldPath));
+ Assert.False(KotzUtilities.TryDeleteDirectory(oldPath));
+ Assert.True(KotzUtilities.TryDeleteDirectory(newPath));
+ Assert.False(Directory.Exists(newPath));
+ }
+
+ [Theory]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ internal void TryMoveDirectorySuccessTests(bool createDirectory, bool expected)
+ {
+ var oldPath = TryDeleteDirectoryTests.CreateDirectoryPath(createDirectory);
+ var newPath = Path.Join(TryDeleteDirectoryTests.CreateDirectoryPath(false), Path.GetFileName(oldPath));
+
+ Assert.Equal(createDirectory, Directory.Exists(oldPath));
+ Assert.Equal(expected, KotzUtilities.TryMoveDirectory(oldPath, newPath));
+
+ if (!createDirectory)
+ return;
+
+ Assert.Equal(!createDirectory, Directory.Exists(oldPath));
+ Assert.False(KotzUtilities.TryDeleteDirectory(oldPath));
+ Assert.True(KotzUtilities.TryDeleteDirectory(newPath));
+ Assert.False(File.Exists(newPath));
+ }
+
+ [Fact]
+ internal void TryMoveDirectoryVolumeTest()
+ {
+ // Define the directory tree
+ var oldRootDirPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Path.GetRandomFileName()); // Root directory
+ var oldDir1Path = Path.Join(oldRootDirPath, Path.GetRandomFileName()); // In: oldRootDirPath > oldDir1Path
+ var oldFile1Path = Path.Join(oldRootDirPath, Path.GetRandomFileName() + ".tmp"); // Inside: oldRootDirPath
+ var oldDir2Path = Path.Join(oldRootDirPath, Path.GetRandomFileName()); // In: oldRootDirPath > oldDir2Path
+ var oldFile2Path = Path.Join(oldDir2Path, Path.GetRandomFileName() + ".tmp"); // Inside: oldRootDirPath > oldDir2Path
+
+ // Create the directory tree
+ Directory.CreateDirectory(oldDir1Path);
+ Directory.CreateDirectory(oldDir2Path);
+ File.Create(oldFile1Path).Dispose();
+ File.Create(oldFile2Path).Dispose();
+
+ // Move to a different volume (ie. "/tmp")
+ var newRootDirPath = Path.Join(Path.GetTempPath(), Path.GetFileName(oldRootDirPath));
+ Assert.True(KotzUtilities.TryMoveDirectory(oldRootDirPath, newRootDirPath));
+
+ // Check if move was successful
+ Assert.False(Directory.Exists(oldRootDirPath));
+ Assert.True(Directory.Exists(newRootDirPath));
+ Assert.True(Directory.Exists(Path.Join(newRootDirPath, Path.GetFileName(oldDir1Path))));
+ Assert.True(Directory.Exists(Path.Join(newRootDirPath, Path.GetFileName(oldDir2Path))));
+ Assert.True(File.Exists(Path.Join(newRootDirPath, Path.GetFileName(oldFile1Path))));
+ Assert.True(File.Exists(Path.Join(newRootDirPath, Path.GetFileName(oldDir2Path), Path.GetFileName(oldFile2Path))));
+
+ // Cleanup
+ Directory.Delete(newRootDirPath, true);
+ Assert.False(Directory.Exists(newRootDirPath));
+ }
+
+ [Fact]
+ internal void TryMoveDirectoryFailTests()
+ {
+ Assert.Throws(() => KotzUtilities.TryMoveDirectory(string.Empty, "not empty"));
+ Assert.Throws(() => KotzUtilities.TryMoveDirectory("not empty", string.Empty));
+ Assert.Throws(() => KotzUtilities.TryMoveDirectory(null!, "not empty"));
+ Assert.Throws(() => KotzUtilities.TryMoveDirectory("not empty", null!));
+ }
+}
\ No newline at end of file
diff --git a/Kotz.Tests/Extensions/Utilities/TryMoveFileTests.cs b/Kotz.Tests/Extensions/Utilities/TryMoveFileTests.cs
new file mode 100644
index 0000000..87ce36e
--- /dev/null
+++ b/Kotz.Tests/Extensions/Utilities/TryMoveFileTests.cs
@@ -0,0 +1,53 @@
+namespace Kotz.Tests.Extensions.Utilities;
+
+public sealed class TryMoveFileTests
+{
+ [Theory]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ internal void TryMoveFileRenameSuccessTests(bool createFile, bool expected)
+ {
+ var oldPath = TryDeleteFileTests.CreateFilePath(createFile);
+ var newPath = TryDeleteFileTests.CreateFilePath(false);
+
+ Assert.Equal(createFile, File.Exists(oldPath));
+ Assert.Equal(expected, KotzUtilities.TryMoveFile(oldPath, newPath));
+
+ if (!createFile)
+ return;
+
+ Assert.Equal(!createFile, File.Exists(oldPath));
+ Assert.False(KotzUtilities.TryDeleteFile(oldPath));
+ Assert.True(KotzUtilities.TryDeleteFile(newPath));
+ Assert.False(File.Exists(newPath));
+ }
+
+ [Theory]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ internal void TryMoveFileSuccessTests(bool createFile, bool expected)
+ {
+ var oldPath = TryDeleteFileTests.CreateFilePath(createFile);
+ var newPath = Path.Join(TryDeleteDirectoryTests.CreateDirectoryPath(false), Path.GetFileName(oldPath));
+
+ Assert.Equal(createFile, File.Exists(oldPath));
+ Assert.Equal(expected, KotzUtilities.TryMoveFile(oldPath, newPath));
+
+ if (!createFile)
+ return;
+
+ Assert.Equal(!createFile, File.Exists(oldPath));
+ Assert.False(KotzUtilities.TryDeleteFile(oldPath));
+ Assert.True(KotzUtilities.TryDeleteFile(newPath));
+ Assert.False(File.Exists(newPath));
+ }
+
+ [Fact]
+ internal void TryMoveFileFailTests()
+ {
+ Assert.Throws(() => KotzUtilities.TryMoveFile(string.Empty, "not empty"));
+ Assert.Throws(() => KotzUtilities.TryMoveFile("not empty", string.Empty));
+ Assert.Throws(() => KotzUtilities.TryMoveFile(null!, "not empty"));
+ Assert.Throws(() => KotzUtilities.TryMoveFile("not empty", null!));
+ }
+}
\ No newline at end of file