diff --git a/FileDeduplicator.Test/ExplicitPathTests.cs b/FileDeduplicator.Test/ExplicitPathTests.cs new file mode 100644 index 0000000..d705f90 --- /dev/null +++ b/FileDeduplicator.Test/ExplicitPathTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.FileDeduplicator.Test; + +using CommandLine; + +using ktsu.FileDeduplicator.Verbs; +using ktsu.Semantics.Paths; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests that an explicit -p . means the current directory rather than "no path given". +/// +/// +/// "." used to be both the option's default and the marker that made a verb prompt, so +/// -p . prompted anyway and read the next stdin line as the path. With input piped in, as in +/// echo y | Deduplicate -p ., the y meant to confirm the deletion became the +/// directory to work on (ktsu-dev/FileDeduplicator#145). +/// +[TestClass] +[DoNotParallelize] +public sealed class ExplicitPathTests +{ + /// + /// Parses a command line the way Program.Main does and returns the verb it selects. + /// + /// The command-line arguments. + /// The parsed verb. + private static BaseVerb Parse(params string[] args) + { + BaseVerb? verb = null; + using Parser parser = new(); + _ = parser.ParseArguments(args, Program.Verbs).WithParsed(parsed => verb = parsed); + Assert.IsNotNull(verb, $"'{string.Join(' ', args)}' did not parse."); + return verb; + } + + /// + /// Runs a verb parsed from with as the + /// current directory. + /// + /// The directory to run in. + /// Lines for the verb's prompts. + /// The command-line arguments. + /// Everything the verb wrote to the console. + private static string RunIn(AbsoluteDirectoryPath directory, string stdin, params string[] args) + { + string original = Environment.CurrentDirectory; + try + { + Environment.CurrentDirectory = directory.WeakString; + return ConsoleCapture.Run(Parse(args), stdin); + } + finally + { + Environment.CurrentDirectory = original; + } + } + + [TestMethod] + public void ScanWithAnExplicitDotScansTheCurrentDirectoryWithoutPrompting() + { + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + _ = tree.Write("aa.txt", "alpha"); + + string output = RunIn(tree.Root, "elsewhere\n", "Scan", "-p", "."); + + Assert.DoesNotContain("Enter the path", output, "An explicit -p . was treated as no path."); + Assert.Contains("Found 1 group(s) of duplicate files", output); + } + + [TestMethod] + public void DeduplicateWithAnExplicitDotReadsTheNextLineAsTheConfirmation() + { + using TempTree tree = new(); + AbsoluteFilePath keeper = tree.Write("a.txt", "alpha"); + AbsoluteFilePath copy = tree.Write("aa.txt", "alpha"); + + string output = RunIn(tree.Root, "y\n", "Deduplicate", "--path", "."); + + Assert.DoesNotContain("Enter the path", output, "An explicit --path . was treated as no path."); + Assert.IsTrue(TempTree.Exists(keeper), "The kept copy was deleted."); + Assert.IsFalse(TempTree.Exists(copy), "The 'y' was not read as the confirmation."); + } + + [TestMethod] + public void DryRunWithAnExplicitDotDoesNotPrompt() + { + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + + string output = RunIn(tree.Root, "elsewhere\n", "DryRun", "-p", "."); + + Assert.DoesNotContain("Enter the path", output); + Assert.Contains("Found 1 file(s).", output); + } + + [TestMethod] + public void StatsWithAnExplicitDotDoesNotPrompt() + { + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + + string output = RunIn(tree.Root, "elsewhere\n", "Stats", "-p", "."); + + Assert.DoesNotContain("Enter the path", output); + Assert.Contains("Total files: 1", output); + } + + [TestMethod] + public void LeavingOutThePathStillPrompts() + { + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + + string output = RunIn(tree.Root, $"{tree.Root.WeakString}\n", "Scan"); + + Assert.Contains("Enter the path to scan:", output); + Assert.Contains("Found 1 file(s).", output); + } +} diff --git a/FileDeduplicator/Verbs/BaseVerb.cs b/FileDeduplicator/Verbs/BaseVerb.cs index 34c0bcf..8017ecf 100644 --- a/FileDeduplicator/Verbs/BaseVerb.cs +++ b/FileDeduplicator/Verbs/BaseVerb.cs @@ -11,12 +11,17 @@ namespace ktsu.FileDeduplicator.Verbs; internal abstract class BaseVerb : ICommand { + /// + /// Gets or sets the path given with -p, or when none was given and the + /// verb should ask for one. An explicit -p . means the current directory, so "." + /// cannot double as the "not supplied" marker. + /// [Option('p', "path", Required = false, HelpText = "The root path to scan for files.")] - public string PathString { get; set; } = "."; + public string? PathString { get; set; } public abstract bool IsActive { get; } - internal AbsoluteDirectoryPath Path => System.IO.Path.GetFullPath(PathString).As(); + internal AbsoluteDirectoryPath Path => System.IO.Path.GetFullPath(PathString ?? ".").As(); public abstract void Run(); @@ -48,7 +53,7 @@ public override void Run() finally { isActive = true; - PathString = "."; + PathString = null; } } diff --git a/FileDeduplicator/Verbs/Deduplicate.cs b/FileDeduplicator/Verbs/Deduplicate.cs index e53eac2..96efc49 100644 --- a/FileDeduplicator/Verbs/Deduplicate.cs +++ b/FileDeduplicator/Verbs/Deduplicate.cs @@ -13,7 +13,7 @@ internal sealed class Deduplicate : BaseVerb { internal override bool ValidateArgs() { - if (PathString is "." or "") + if (string.IsNullOrWhiteSpace(PathString)) { Console.Write("Enter the path to deduplicate: "); string? input = Console.ReadLine()?.Trim(); diff --git a/FileDeduplicator/Verbs/DryRun.cs b/FileDeduplicator/Verbs/DryRun.cs index 3b5d70f..6bdd9b0 100644 --- a/FileDeduplicator/Verbs/DryRun.cs +++ b/FileDeduplicator/Verbs/DryRun.cs @@ -13,7 +13,7 @@ internal sealed class DryRun : BaseVerb { internal override bool ValidateArgs() { - if (PathString is "." or "") + if (string.IsNullOrWhiteSpace(PathString)) { Console.Write("Enter the path to scan: "); string? input = Console.ReadLine()?.Trim(); diff --git a/FileDeduplicator/Verbs/Scan.cs b/FileDeduplicator/Verbs/Scan.cs index a519d91..a2df2b3 100644 --- a/FileDeduplicator/Verbs/Scan.cs +++ b/FileDeduplicator/Verbs/Scan.cs @@ -13,7 +13,7 @@ internal sealed class Scan : BaseVerb { internal override bool ValidateArgs() { - if (PathString is "." or "") + if (string.IsNullOrWhiteSpace(PathString)) { Console.Write("Enter the path to scan: "); string? input = Console.ReadLine()?.Trim(); diff --git a/FileDeduplicator/Verbs/Stats.cs b/FileDeduplicator/Verbs/Stats.cs index 097a25b..45d1acb 100644 --- a/FileDeduplicator/Verbs/Stats.cs +++ b/FileDeduplicator/Verbs/Stats.cs @@ -14,7 +14,7 @@ internal sealed class Stats : BaseVerb { internal override bool ValidateArgs() { - if (PathString is "." or "") + if (string.IsNullOrWhiteSpace(PathString)) { Console.Write("Enter the path to analyze: "); string? input = Console.ReadLine()?.Trim();