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}");