diff --git a/FileDeduplicator.Test/FileScannerAndHasherTests.cs b/FileDeduplicator.Test/FileScannerAndHasherTests.cs
index cb97c57..e059b5e 100644
--- a/FileDeduplicator.Test/FileScannerAndHasherTests.cs
+++ b/FileDeduplicator.Test/FileScannerAndHasherTests.cs
@@ -68,6 +68,25 @@ public void ScanOfAMissingDirectoryReturnsNothing()
Assert.IsEmpty(files);
}
+ ///
+ /// A path to a file, rather than the folder holding it, must return an empty list rather than
+ /// throwing, so pasting a file path does not crash the tool.
+ ///
+ [TestMethod]
+ public void ScanOfAFileReturnsNothing()
+ {
+ // Arrange
+ using TempTree tree = new();
+ AbsoluteFilePath file = tree.Write("a/f.txt", "content");
+ AbsoluteDirectoryPath notADirectory = file.WeakString.As();
+
+ // Act
+ IReadOnlyList files = FileScanner.ScanForFiles(notADirectory);
+
+ // Assert
+ Assert.IsEmpty(files);
+ }
+
///
/// One unreadable directory must not abort the scan. Enumeration is lazy, so the refusal
/// arrives partway through the walk, after files elsewhere in the tree have already been found
diff --git a/FileDeduplicator.Test/VerbOutputTests.cs b/FileDeduplicator.Test/VerbOutputTests.cs
index f650ade..4aad89d 100644
--- a/FileDeduplicator.Test/VerbOutputTests.cs
+++ b/FileDeduplicator.Test/VerbOutputTests.cs
@@ -36,6 +36,35 @@ private static void WriteOneGroup(TempTree tree, out AbsoluteFilePath keeper, ou
_ = tree.Write("unique.txt", "beta");
}
+ ///
+ /// Every verb given a file instead of a directory says so in one line and deletes nothing.
+ ///
+ [TestMethod]
+ public void EveryVerbReportsAFilePathAsNotADirectory()
+ {
+ // Arrange
+ using TempTree tree = new();
+ AbsoluteFilePath file = tree.Write("a/f.txt", "content");
+ string path = file.WeakString;
+
+ // Act
+ string[] outputs =
+ [
+ ConsoleCapture.Run(new Scan { PathString = path }),
+ ConsoleCapture.Run(new DryRun { PathString = path }),
+ ConsoleCapture.Run(new Stats { PathString = path }),
+ ConsoleCapture.Run(new Deduplicate { PathString = path }, "y"),
+ ];
+
+ // Assert
+ foreach (string output in outputs)
+ {
+ Assert.Contains($"Not a directory: {path}", output);
+ }
+
+ Assert.IsTrue(TempTree.Exists(file), "The file named as the root must be left alone.");
+ }
+
///
/// DryRun names the keeper and every other copy, totals them, and deletes nothing.
///
diff --git a/FileDeduplicator/FileScanner.cs b/FileDeduplicator/FileScanner.cs
index c9db82f..ed4ccca 100644
--- a/FileDeduplicator/FileScanner.cs
+++ b/FileDeduplicator/FileScanner.cs
@@ -44,7 +44,16 @@ internal static class FileScanner
internal static IReadOnlyList ScanForFiles(AbsoluteDirectoryPath path)
{
- if (!path.Exists)
+ // path.Exists is also true for a file, which Directory.EnumerateFiles then rejects with an
+ // unhandled DirectoryNotFoundException. A path tab-completed one level too far is an easy
+ // mistake, so it gets the same one-line error as a path that is not there at all.
+ if (File.Exists(path.WeakString))
+ {
+ Console.WriteLine($"Not a directory: {path}");
+ return [];
+ }
+
+ if (!Directory.Exists(path.WeakString))
{
Console.WriteLine($"Directory not found: {path}");
return [];