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