From e48434ffaf4794715bdd01e6c5877790aa17642c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 19:28:57 +0000 Subject: [PATCH] fix: stop Stats counting files it could not hash as duplicates [patch] Stats derived "Duplicate files" from the scanned list, but HashFiles drops a file it cannot read, so each unreadable file was reported as a duplicate of nothing. The counts and the total size now come from the files that hashed, and the files that did not are reported on their own "Unreadable files" line. Fixes ktsu-dev/FileDeduplicator#137 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YLYtHwwqdY3Tk9vpkwT6Sw --- FileDeduplicator.Test/VerbOutputTests.cs | 48 ++++++++++++++++++++++++ FileDeduplicator/Verbs/Stats.cs | 13 ++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/FileDeduplicator.Test/VerbOutputTests.cs b/FileDeduplicator.Test/VerbOutputTests.cs index 42d8f80..f650ade 100644 --- a/FileDeduplicator.Test/VerbOutputTests.cs +++ b/FileDeduplicator.Test/VerbOutputTests.cs @@ -121,6 +121,54 @@ public void StatsReportsTotalsForTheTree() Assert.IsTrue(doomed.TrueForAll(TempTree.Exists), "Stats deleted a file."); } + /// + /// A file Stats cannot hash is reported as unreadable, not counted as a duplicate or in the size. + /// + /// + /// Held open with rather than stripped of its permissions, because a + /// process running as root reads through permissions but .NET enforces the share lock on every + /// platform. Before ktsu-dev/FileDeduplicator#137 this tree reported one duplicate file and zero + /// duplicate groups. + /// + [TestMethod] + public void StatsReportsAFileItCannotHashAsUnreadable() + { + // Arrange + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + _ = tree.Write("b.txt", "beta"); + AbsoluteFilePath locked = tree.Write("locked.txt", "gamma"); + using FileStream holder = new(locked.WeakString, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + + // Act + string output = ConsoleCapture.Normalize(ConsoleCapture.Run(new Stats { PathString = tree.Root.WeakString })); + + // Assert -- the two readable files are 9 B and unique + Assert.Contains("Total files: 3", output); + Assert.Contains("Unreadable files: 1", output); + Assert.Contains("Total size: 9 B", output); + Assert.Contains("Unique files: 2", output); + Assert.Contains("Duplicate files: 0", output); + Assert.Contains("Duplicate groups: 0", output); + } + + /// + /// A tree where every file hashes has no unreadable line at all. + /// + [TestMethod] + public void StatsOmitsTheUnreadableLineWhenEveryFileHashes() + { + // Arrange + using TempTree tree = new(); + WriteOneGroup(tree, out _, out _); + + // Act + string output = ConsoleCapture.Normalize(ConsoleCapture.Run(new Stats { PathString = tree.Root.WeakString })); + + // Assert + Assert.DoesNotContain("Unreadable files:", output); + } + /// /// A tree with nothing duplicated says so, in every verb that looks for duplicates. /// diff --git a/FileDeduplicator/Verbs/Stats.cs b/FileDeduplicator/Verbs/Stats.cs index 2ad684f..f04bbc4 100644 --- a/FileDeduplicator/Verbs/Stats.cs +++ b/FileDeduplicator/Verbs/Stats.cs @@ -55,13 +55,22 @@ internal override void Run(Stats options) Dictionary> hashGroups = Deduplicator.GroupByHash(fileHashes); IReadOnlyList duplicates = Deduplicator.FindDuplicates(hashGroups); - long totalSize = files.Sum(f => new FileInfo(f.WeakString).Length); + // Every count below is taken over the files that hashed. HashFiles drops a file it cannot + // read, so counting against the scanned list would report each one as a duplicate of nothing, + // and sizing it would throw if it had vanished since the scan. + long totalSize = fileHashes.Keys.Sum(f => new FileInfo(f.WeakString).Length); int uniqueFiles = hashGroups.Count; - int duplicateFiles = files.Count - uniqueFiles; + int duplicateFiles = fileHashes.Count - uniqueFiles; + int unreadableFiles = files.Count - fileHashes.Count; Console.WriteLine("=== FileDeduplicator Statistics ==="); Console.WriteLine(); Console.WriteLine($"Total files: {files.Count}"); + if (unreadableFiles > 0) + { + Console.WriteLine($"Unreadable files: {unreadableFiles}"); + } + Console.WriteLine($"Total size: {DuplicateReport.FormatBytes(totalSize)}"); Console.WriteLine($"Unique files: {uniqueFiles}"); Console.WriteLine($"Duplicate files: {duplicateFiles}");