From 49b09f19e0e19dbb5598207976b5f30c83daecbf Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 10 Jul 2026 13:28:26 -0700 Subject: [PATCH 1/4] Cover synchronous FileEx methods with temp-directory tests --- UtilitiesTests/FileExTests.cs | 300 ++++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 UtilitiesTests/FileExTests.cs diff --git a/UtilitiesTests/FileExTests.cs b/UtilitiesTests/FileExTests.cs new file mode 100644 index 0000000..f193896 --- /dev/null +++ b/UtilitiesTests/FileExTests.cs @@ -0,0 +1,300 @@ +using System.Collections.ObjectModel; + +namespace ptr727.Utilities.Tests; + +public sealed class FileExTests : IDisposable +{ + private readonly string _tempDir = Path.Combine( + Path.GetTempPath(), + $"FileExTests-{Guid.NewGuid()}" + ); + + public FileExTests() => Directory.CreateDirectory(_tempDir); + + public void Dispose() + { + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, recursive: true); + } + } + + private string NewFile(string name, string content = "content") + { + string path = Path.Combine(_tempDir, name); + File.WriteAllText(path, content); + return path; + } + + private string NewDir(string name) + { + string path = Path.Combine(_tempDir, name); + _ = Directory.CreateDirectory(path); + return path; + } + + [Fact] + public void DeleteFile_ExistingFile_DeletesAndReturnsTrue() + { + string file = NewFile("delete.txt"); + + _ = FileEx.DeleteFile(file).Should().BeTrue(); + _ = File.Exists(file).Should().BeFalse(); + } + + [Fact] + public void DeleteFile_MissingFile_ReturnsTrue() => + FileEx.DeleteFile(Path.Combine(_tempDir, "missing.txt")).Should().BeTrue(); + + [Fact] + public void DeleteDirectory_ExistingEmpty_DeletesAndReturnsTrue() + { + string dir = NewDir("empty"); + + _ = FileEx.DeleteDirectory(dir).Should().BeTrue(); + _ = Directory.Exists(dir).Should().BeFalse(); + } + + [Fact] + public void DeleteDirectory_Missing_ReturnsTrue() => + FileEx.DeleteDirectory(Path.Combine(_tempDir, "missing")).Should().BeTrue(); + + [Fact] + public void DeleteDirectory_Recursive_DeletesContents() + { + string dir = NewDir("tree"); + string sub = Path.Combine(dir, "sub"); + _ = Directory.CreateDirectory(sub); + File.WriteAllText(Path.Combine(sub, "file.txt"), "x"); + + _ = FileEx.DeleteDirectory(dir, recursive: true).Should().BeTrue(); + _ = Directory.Exists(dir).Should().BeFalse(); + } + + [Fact] + public void RenameFile_ToNewName_MovesFile() + { + string source = NewFile("source.txt", "data"); + string target = Path.Combine(_tempDir, "target.txt"); + + _ = FileEx.RenameFile(source, target).Should().BeTrue(); + _ = File.Exists(source).Should().BeFalse(); + _ = File.ReadAllText(target).Should().Be("data"); + } + + [Fact] + public void RenameFile_OverExistingDestination_Overwrites() + { + string source = NewFile("source.txt", "new"); + string target = NewFile("target.txt", "old"); + + _ = FileEx.RenameFile(source, target).Should().BeTrue(); + _ = File.ReadAllText(target).Should().Be("new"); + } + + [Fact] + public void RenameFile_InvalidPath_ReturnsFalse() => + FileEx.RenameFile("nodirectory.txt", Path.Combine(_tempDir, "x.txt")).Should().BeFalse(); + + [Fact] + public void RenameFile_MissingSource_ReturnsFalse() => + FileEx + .RenameFile(Path.Combine(_tempDir, "missing.txt"), Path.Combine(_tempDir, "x.txt")) + .Should() + .BeFalse(); + + [Fact] + public void RenameFolder_ToNewName_MovesFolder() + { + string source = NewDir("source"); + File.WriteAllText(Path.Combine(source, "file.txt"), "x"); + string target = Path.Combine(_tempDir, "target"); + + _ = FileEx.RenameFolder(source, target).Should().BeTrue(); + _ = Directory.Exists(source).Should().BeFalse(); + _ = File.Exists(Path.Combine(target, "file.txt")).Should().BeTrue(); + } + + [Fact] + public void RenameFolder_OverExistingDestination_Overwrites() + { + string source = NewDir("source"); + File.WriteAllText(Path.Combine(source, "new.txt"), "x"); + string target = NewDir("target"); + File.WriteAllText(Path.Combine(target, "old.txt"), "y"); + + _ = FileEx.RenameFolder(source, target).Should().BeTrue(); + _ = File.Exists(Path.Combine(target, "new.txt")).Should().BeTrue(); + _ = File.Exists(Path.Combine(target, "old.txt")).Should().BeFalse(); + } + + [Fact] + public void DeleteEmptyDirectories_RemovesEmptyAndCounts() + { + string dir = NewDir("root"); + _ = Directory.CreateDirectory(Path.Combine(dir, "emptyA")); + _ = Directory.CreateDirectory(Path.Combine(dir, "emptyB")); + + int deleted = 0; + _ = FileEx.DeleteEmptyDirectories(dir, ref deleted).Should().BeTrue(); + _ = deleted.Should().Be(2); + _ = Directory.EnumerateDirectories(dir).Should().BeEmpty(); + } + + [Fact] + public void DeleteEmptyDirectories_KeepsNonEmpty() + { + string dir = NewDir("root"); + string keep = Path.Combine(dir, "keep"); + _ = Directory.CreateDirectory(keep); + File.WriteAllText(Path.Combine(keep, "file.txt"), "x"); + + int deleted = 0; + _ = FileEx.DeleteEmptyDirectories(dir, ref deleted).Should().BeTrue(); + _ = deleted.Should().Be(0); + _ = Directory.Exists(keep).Should().BeTrue(); + } + + [Fact] + public void DeleteInsideDirectory_RemovesContentsKeepsDirectory() + { + string dir = NewDir("root"); + File.WriteAllText(Path.Combine(dir, "file.txt"), "x"); + string sub = Path.Combine(dir, "sub"); + _ = Directory.CreateDirectory(sub); + File.WriteAllText(Path.Combine(sub, "nested.txt"), "y"); + + _ = FileEx.DeleteInsideDirectory(dir).Should().BeTrue(); + _ = Directory.Exists(dir).Should().BeTrue(); + _ = Directory.EnumerateFileSystemEntries(dir).Should().BeEmpty(); + } + + [Fact] + public void DeleteInsideDirectory_Missing_ReturnsTrue() => + FileEx.DeleteInsideDirectory(Path.Combine(_tempDir, "missing")).Should().BeTrue(); + + [Fact] + public void IsFileReadable_String_ReadableFile_ReturnsTrue() => + FileEx.IsFileReadable(NewFile("readable.txt")).Should().BeTrue(); + + [Fact] + public void IsFileReadable_String_Missing_ReturnsFalse() => + FileEx.IsFileReadable(Path.Combine(_tempDir, "missing.txt")).Should().BeFalse(); + + [Fact] + public void IsFileReadable_FileInfo_ReturnsTrue() => + FileEx.IsFileReadable(new FileInfo(NewFile("read.txt"))).Should().BeTrue(); + + [Fact] + public void IsFileWriteable_FileInfo_ReturnsTrue() => + FileEx.IsFileWriteable(new FileInfo(NewFile("write.txt"))).Should().BeTrue(); + + [Fact] + public void IsFileReadWriteable_FileInfo_ReturnsTrue() => + FileEx.IsFileReadWriteable(new FileInfo(NewFile("readwrite.txt"))).Should().BeTrue(); + + [Fact] + public void IsFileReadable_NullFileInfo_Throws() => + FluentActions + .Invoking(() => FileEx.IsFileReadable((FileInfo)null!)) + .Should() + .Throw(); + + [Fact] + public void WaitFileReadable_ReadableFile_ReturnsTrue() => + FileEx.WaitFileReadable(NewFile("wait.txt")).Should().BeTrue(); + + [Fact] + public void AreFilesInDirectoryReadable_AllReadable_ReturnsTrue() + { + _ = NewFile("a.txt"); + _ = NewFile("b.txt"); + + _ = FileEx.AreFilesInDirectoryReadable(_tempDir).Should().BeTrue(); + } + + [Fact] + public void CreateDirectory_New_CreatesIt() + { + string dir = Path.Combine(_tempDir, "created"); + + _ = FileEx.CreateDirectory(dir).Should().BeTrue(); + _ = Directory.Exists(dir).Should().BeTrue(); + } + + [Fact] + public void CreateDirectory_Existing_ReturnsTrue() => + FileEx.CreateDirectory(_tempDir).Should().BeTrue(); + + [Fact] + public void EnumerateDirectory_ReturnsFilesAndDirectories() + { + _ = NewFile("file.txt"); + string sub = NewDir("sub"); + + bool result = FileEx.EnumerateDirectory( + _tempDir, + out Collection files, + out Collection directories + ); + + _ = result.Should().BeTrue(); + _ = directories.Select(d => d.FullName).Should().Contain([_tempDir, sub]); + _ = files.Select(f => f.Name).Should().Contain("file.txt"); + } + + [Fact] + public void EnumerateDirectories_MultipleSources_ReturnsAll() + { + string dirA = NewDir("A"); + string dirB = NewDir("B"); + File.WriteAllText(Path.Combine(dirA, "a.txt"), "x"); + File.WriteAllText(Path.Combine(dirB, "b.txt"), "y"); + + bool result = FileEx.EnumerateDirectories( + [dirA, dirB], + out Collection files, + out Collection directories + ); + + _ = result.Should().BeTrue(); + _ = directories.Select(d => d.FullName).Should().Contain([dirA, dirB]); + _ = files.Select(f => f.Name).Should().Contain(["a.txt", "b.txt"]); + } + + [Fact] + public void TimeStampFileName_PrefixesTimestamp() + { + string path = Path.Combine(_tempDir, "report.log"); + DateTime timeStamp = new(2026, 7, 10, 14, 30, 15, DateTimeKind.Utc); + + string result = FileEx.TimeStampFileName(path, timeStamp); + + _ = Path.GetFileName(result).Should().Be("20260710T143015_report.log"); + _ = Path.GetDirectoryName(result).Should().Be(_tempDir); + } + + [Fact] + public void TimeStampFileName_DefaultTimestamp_PrefixesFileName() => + Path.GetFileName(FileEx.TimeStampFileName(Path.Combine(_tempDir, "report.log"))) + .Should() + .EndWith("_report.log"); + + [Fact] + public void CreateRandomFilledFile_CreatesFileOfSize() + { + string path = Path.Combine(_tempDir, "random.bin"); + + _ = FileEx.CreateRandomFilledFile(path, 2048).Should().BeTrue(); + _ = new FileInfo(path).Length.Should().Be(2048); + } + + [Fact] + public void CreateSparseFile_CreatesFileOfSize() + { + string path = Path.Combine(_tempDir, "sparse.bin"); + + _ = FileEx.CreateSparseFile(path, 4096).Should().BeTrue(); + _ = new FileInfo(path).Length.Should().Be(4096); + } +} From bfeaf3be039b6f9e8de1391c6c00729f50363dad Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 10 Jul 2026 13:32:41 -0700 Subject: [PATCH 2/4] Rename RenameFile test to reflect the missing-directory scenario --- UtilitiesTests/FileExTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UtilitiesTests/FileExTests.cs b/UtilitiesTests/FileExTests.cs index f193896..89117e0 100644 --- a/UtilitiesTests/FileExTests.cs +++ b/UtilitiesTests/FileExTests.cs @@ -93,7 +93,7 @@ public void RenameFile_OverExistingDestination_Overwrites() } [Fact] - public void RenameFile_InvalidPath_ReturnsFalse() => + public void RenameFile_SourceWithoutDirectory_ReturnsFalse() => FileEx.RenameFile("nodirectory.txt", Path.Combine(_tempDir, "x.txt")).Should().BeFalse(); [Fact] From c4eaef31b3c5be334cd4e1b1d35f6e073c63d9d8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 10 Jul 2026 13:37:47 -0700 Subject: [PATCH 3/4] Use discard assignment in test constructor for consistency --- UtilitiesTests/FileExTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UtilitiesTests/FileExTests.cs b/UtilitiesTests/FileExTests.cs index 89117e0..b8cd621 100644 --- a/UtilitiesTests/FileExTests.cs +++ b/UtilitiesTests/FileExTests.cs @@ -9,7 +9,7 @@ public sealed class FileExTests : IDisposable $"FileExTests-{Guid.NewGuid()}" ); - public FileExTests() => Directory.CreateDirectory(_tempDir); + public FileExTests() => _ = Directory.CreateDirectory(_tempDir); public void Dispose() { From 312f3444075f328db24135eea3a5028bd74771cf Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 10 Jul 2026 13:44:19 -0700 Subject: [PATCH 4/4] Use assertion discard in expression-bodied FileEx tests --- UtilitiesTests/FileExTests.cs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/UtilitiesTests/FileExTests.cs b/UtilitiesTests/FileExTests.cs index b8cd621..d18198c 100644 --- a/UtilitiesTests/FileExTests.cs +++ b/UtilitiesTests/FileExTests.cs @@ -44,7 +44,7 @@ public void DeleteFile_ExistingFile_DeletesAndReturnsTrue() [Fact] public void DeleteFile_MissingFile_ReturnsTrue() => - FileEx.DeleteFile(Path.Combine(_tempDir, "missing.txt")).Should().BeTrue(); + _ = FileEx.DeleteFile(Path.Combine(_tempDir, "missing.txt")).Should().BeTrue(); [Fact] public void DeleteDirectory_ExistingEmpty_DeletesAndReturnsTrue() @@ -57,7 +57,7 @@ public void DeleteDirectory_ExistingEmpty_DeletesAndReturnsTrue() [Fact] public void DeleteDirectory_Missing_ReturnsTrue() => - FileEx.DeleteDirectory(Path.Combine(_tempDir, "missing")).Should().BeTrue(); + _ = FileEx.DeleteDirectory(Path.Combine(_tempDir, "missing")).Should().BeTrue(); [Fact] public void DeleteDirectory_Recursive_DeletesContents() @@ -94,11 +94,14 @@ public void RenameFile_OverExistingDestination_Overwrites() [Fact] public void RenameFile_SourceWithoutDirectory_ReturnsFalse() => - FileEx.RenameFile("nodirectory.txt", Path.Combine(_tempDir, "x.txt")).Should().BeFalse(); + _ = FileEx + .RenameFile("nodirectory.txt", Path.Combine(_tempDir, "x.txt")) + .Should() + .BeFalse(); [Fact] public void RenameFile_MissingSource_ReturnsFalse() => - FileEx + _ = FileEx .RenameFile(Path.Combine(_tempDir, "missing.txt"), Path.Combine(_tempDir, "x.txt")) .Should() .BeFalse(); @@ -171,38 +174,38 @@ public void DeleteInsideDirectory_RemovesContentsKeepsDirectory() [Fact] public void DeleteInsideDirectory_Missing_ReturnsTrue() => - FileEx.DeleteInsideDirectory(Path.Combine(_tempDir, "missing")).Should().BeTrue(); + _ = FileEx.DeleteInsideDirectory(Path.Combine(_tempDir, "missing")).Should().BeTrue(); [Fact] public void IsFileReadable_String_ReadableFile_ReturnsTrue() => - FileEx.IsFileReadable(NewFile("readable.txt")).Should().BeTrue(); + _ = FileEx.IsFileReadable(NewFile("readable.txt")).Should().BeTrue(); [Fact] public void IsFileReadable_String_Missing_ReturnsFalse() => - FileEx.IsFileReadable(Path.Combine(_tempDir, "missing.txt")).Should().BeFalse(); + _ = FileEx.IsFileReadable(Path.Combine(_tempDir, "missing.txt")).Should().BeFalse(); [Fact] public void IsFileReadable_FileInfo_ReturnsTrue() => - FileEx.IsFileReadable(new FileInfo(NewFile("read.txt"))).Should().BeTrue(); + _ = FileEx.IsFileReadable(new FileInfo(NewFile("read.txt"))).Should().BeTrue(); [Fact] public void IsFileWriteable_FileInfo_ReturnsTrue() => - FileEx.IsFileWriteable(new FileInfo(NewFile("write.txt"))).Should().BeTrue(); + _ = FileEx.IsFileWriteable(new FileInfo(NewFile("write.txt"))).Should().BeTrue(); [Fact] public void IsFileReadWriteable_FileInfo_ReturnsTrue() => - FileEx.IsFileReadWriteable(new FileInfo(NewFile("readwrite.txt"))).Should().BeTrue(); + _ = FileEx.IsFileReadWriteable(new FileInfo(NewFile("readwrite.txt"))).Should().BeTrue(); [Fact] public void IsFileReadable_NullFileInfo_Throws() => - FluentActions + _ = FluentActions .Invoking(() => FileEx.IsFileReadable((FileInfo)null!)) .Should() .Throw(); [Fact] public void WaitFileReadable_ReadableFile_ReturnsTrue() => - FileEx.WaitFileReadable(NewFile("wait.txt")).Should().BeTrue(); + _ = FileEx.WaitFileReadable(NewFile("wait.txt")).Should().BeTrue(); [Fact] public void AreFilesInDirectoryReadable_AllReadable_ReturnsTrue() @@ -224,7 +227,7 @@ public void CreateDirectory_New_CreatesIt() [Fact] public void CreateDirectory_Existing_ReturnsTrue() => - FileEx.CreateDirectory(_tempDir).Should().BeTrue(); + _ = FileEx.CreateDirectory(_tempDir).Should().BeTrue(); [Fact] public void EnumerateDirectory_ReturnsFilesAndDirectories() @@ -276,7 +279,7 @@ public void TimeStampFileName_PrefixesTimestamp() [Fact] public void TimeStampFileName_DefaultTimestamp_PrefixesFileName() => - Path.GetFileName(FileEx.TimeStampFileName(Path.Combine(_tempDir, "report.log"))) + _ = Path.GetFileName(FileEx.TimeStampFileName(Path.Combine(_tempDir, "report.log"))) .Should() .EndWith("_report.log");