Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions FileDeduplicator.Test/VerbReuseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.FileDeduplicator.Test;

using ktsu.FileDeduplicator.Verbs;
using ktsu.Semantics.Paths;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Tests that a verb instance asks for a new path each time it runs, however its last run ended.
/// </summary>
/// <remarks>
/// The interactive menu creates one instance of each verb and runs it every time its item is chosen.
/// Each verb reset its path only on the last line of its run, so an early return (nothing found, no
/// duplicates, a declined confirmation) kept the old path. The next run then skipped the prompt,
/// and the path the user typed was read as the answer to the next question instead
/// (ktsu-dev/FileDeduplicator#138).
/// </remarks>
[TestClass]
[DoNotParallelize]
public sealed class VerbReuseTests
{
/// <summary>
/// Runs a verb against a tree with no duplicates, which returns early, then runs it again.
/// </summary>
/// <param name="verb">The verb instance, reused as the menu reuses it.</param>
/// <param name="prompt">The prompt the verb shows when it has no path.</param>
private static void AssertPromptsAgainAfterAnEarlyReturn(BaseVerb verb, string prompt)
{
using TempTree first = new();
_ = first.Write("a.txt", "alpha");
_ = first.Write("b.txt", "beta");

using TempTree second = new();
_ = second.Write("c.txt", "gamma");

string firstOutput = ConsoleCapture.Run(verb, $"{first.Root.WeakString}\n");
string secondOutput = ConsoleCapture.Run(verb, $"{second.Root.WeakString}\n");

Assert.Contains(prompt, firstOutput);
Assert.Contains(prompt, secondOutput, $"{verb.GetType().Name} reused the previous path without asking.");
Assert.DoesNotContain(first.Root.WeakString, secondOutput, $"{verb.GetType().Name} ran against the previous path.");
}

[TestMethod]
public void DeduplicatePromptsAgainAfterFindingNoDuplicates() =>
AssertPromptsAgainAfterAnEarlyReturn(new Deduplicate(), "Enter the path to deduplicate:");

[TestMethod]
public void DryRunPromptsAgainAfterFindingNoDuplicates() =>
AssertPromptsAgainAfterAnEarlyReturn(new DryRun(), "Enter the path to scan:");

[TestMethod]
public void ScanPromptsAgainAfterFindingNoDuplicates() =>
AssertPromptsAgainAfterAnEarlyReturn(new Scan(), "Enter the path to scan:");

[TestMethod]
public void StatsPromptsAgainAfterFindingNoFiles()
{
Stats stats = new();
using TempTree empty = new();
using TempTree second = new();
_ = second.Write("c.txt", "gamma");

_ = ConsoleCapture.Run(stats, $"{empty.Root.WeakString}\n");
string secondOutput = ConsoleCapture.Run(stats, $"{second.Root.WeakString}\n");

Assert.Contains("Enter the path to analyze:", secondOutput, "Stats reused the previous path without asking.");
}

/// <summary>
/// Declining the deletion is the case the issue describes: the next path typed must not be read
/// as a confirmation against the previous directory.
/// </summary>
[TestMethod]
public void DeduplicatePromptsAgainAfterTheUserDeclines()
{
Deduplicate deduplicate = new();
using TempTree first = new();
AbsoluteFilePath keeper = first.Write("a.txt", "alpha");
AbsoluteFilePath copy = first.Write("aa.txt", "alpha");

using TempTree second = new();
_ = second.Write("c.txt", "gamma");

_ = ConsoleCapture.Run(deduplicate, $"{first.Root.WeakString}\nn\n");
string secondOutput = ConsoleCapture.Run(deduplicate, $"{second.Root.WeakString}\n");

Assert.Contains("Enter the path to deduplicate:", secondOutput, "Deduplicate reused the previous path without asking.");
Assert.IsTrue(TempTree.Exists(keeper) && TempTree.Exists(copy), "The path typed second was read as a confirmation against the first directory.");
}
}
22 changes: 16 additions & 6 deletions FileDeduplicator/Verbs/BaseVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,24 @@ internal abstract class BaseVerb<T> : BaseVerb where T : BaseVerb<T>

public override void Run()
{
if (!ValidateArgs())
// The interactive menu runs the same instance every time its item is chosen, so the path is
// put back on every exit, early returns and exceptions included. Otherwise the next run skips
// the prompt and works on the previous directory.
try
{
return;
}
if (!ValidateArgs())
{
return;
}

isActive = false;
Run((T)this);
isActive = true;
isActive = false;
Run((T)this);
}
finally
{
isActive = true;
PathString = ".";
}
}

internal abstract void Run(T options);
Expand Down
2 changes: 0 additions & 2 deletions FileDeduplicator/Verbs/Deduplicate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,5 @@ internal override void Run(Deduplicate options)
{
Console.WriteLine($"Encountered {result.Errors.Count} error(s) during deletion.");
}

PathString = ".";
}
}
2 changes: 0 additions & 2 deletions FileDeduplicator/Verbs/DryRun.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,5 @@ internal override void Run(DryRun options)
Console.WriteLine($"Duplicate groups: {duplicates.Count}");
Console.WriteLine($"Files to delete: {plan.FileCount}");
Console.WriteLine($"Space to reclaim: {DuplicateReport.FormatBytes(plan.BytesReclaimable)}");

PathString = ".";
}
}
2 changes: 0 additions & 2 deletions FileDeduplicator/Verbs/Scan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,5 @@ internal override void Run(Scan options)
Console.WriteLine($"Total wasted space: {DuplicateReport.FormatBytes(totalWastedBytes)}");
Console.WriteLine();
Console.WriteLine("Run the 'Deduplicate' command to remove duplicates.");

PathString = ".";
}
}
2 changes: 0 additions & 2 deletions FileDeduplicator/Verbs/Stats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@

// Extension breakdown
Dictionary<string, int> extensionCounts = [];
foreach (DuplicateGroup group in duplicates)

Check warning on line 78 in FileDeduplicator/Verbs/Stats.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(group => group.Files))

Check warning on line 78 in FileDeduplicator/Verbs/Stats.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(group => group.Files))

Check warning on line 78 in FileDeduplicator/Verbs/Stats.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(group => group.Files))

Check warning on line 78 in FileDeduplicator/Verbs/Stats.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(group => group.Files))
{
string ext = System.IO.Path.GetExtension(group.Files[0].WeakString);
if (string.IsNullOrEmpty(ext))
Expand Down Expand Up @@ -104,7 +104,5 @@
Console.WriteLine($" {group.Hash[..12]}... - {group.Files.Count} copies, {DuplicateReport.FormatBytes(group.FileSize)} each, {DuplicateReport.FormatBytes(wasted)} wasted");
}
}

PathString = ".";
}
}
Loading