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
25 changes: 25 additions & 0 deletions FileDeduplicator.Test/DeduplicateConfirmationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,29 @@ public void ConfirmingDeletesExactlyTheCopiesTheListingNamed()
Assert.Contains("Deleted 1 file(s).", output);
Assert.Contains("Reclaimed 5 B of disk space.", output);
}

/// <summary>
/// Empty files are never deleted, so a tree whose only duplicates are empty marker files has
/// nothing to approve: the verb says so, reads no answer, and succeeds.
/// </summary>
[TestMethod]
public void OnlyEmptyDuplicatesSkipTheConfirmationAndSucceed()
{
// Arrange -- the routine Python/git case: two empty __init__.py files and one unique file
using TempTree tree = new();
AbsoluteFilePath first = tree.Write("a/__init__.py", string.Empty);
AbsoluteFilePath second = tree.Write("b/__init__.py", string.Empty);
_ = tree.Write("u.txt", "unique");

// Act -- stdin is empty, as it is for a script with nothing piped in
string output = ConsoleCapture.Normalize(ConsoleCapture.Run(new Deduplicate { PathString = tree.Root.WeakString }, string.Empty, out int exitCode));

// Assert
Assert.AreEqual(0, exitCode, $"A run with nothing to delete reported failure. Output was:\n{output}");
Assert.Contains("Nothing to delete", output);
Assert.DoesNotContain("Proceed with deletion?", output, $"The verb asked to approve deleting nothing. Output was:\n{output}");
Assert.DoesNotContain("Aborted.", output);
Assert.IsTrue(TempTree.Exists(first), $"{first} is empty and should never have been touched.");
Assert.IsTrue(TempTree.Exists(second), $"{second} is empty and should never have been touched.");
}
}
20 changes: 20 additions & 0 deletions FileDeduplicator.Test/VerbOutputTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -439,4 +439,24 @@ private static (string Output, int ExitCode) RunWithExitCode(BaseVerb verb, stri
string output = ConsoleCapture.Run(verb, stdin, out int exitCode);
return (output, exitCode);
}

/// <summary>
/// When every duplicate is empty, and so kept, Scan does not point at a Deduplicate run that
/// would have nothing to do.
/// </summary>
[TestMethod]
public void ScanDoesNotSuggestDeduplicateWhenOnlyEmptyDuplicatesExist()
{
// Arrange
using TempTree tree = new();
AbsoluteFilePath first = tree.Write("a/.gitkeep", string.Empty);
_ = tree.Write("b/.gitkeep", string.Empty);

// Act
string output = ConsoleCapture.Normalize(ConsoleCapture.Run(new Scan { PathString = tree.Root.WeakString }));

// Assert
Assert.Contains($"{first} [KEEP, empty]", output);
Assert.DoesNotContain("Run the 'Deduplicate' command", output, $"Scan suggested a Deduplicate run with nothing to delete. Output was:\n{output}");
}
}
9 changes: 9 additions & 0 deletions FileDeduplicator/Verbs/Deduplicate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ internal override void Run(Deduplicate options)
// and DryRun already print this; the verb that actually deletes is the one that needs it.
DeletionPlan plan = DuplicateReport.PlanDeletions(duplicates);

// Empty files are never deleted, so a tree whose only duplicates are empty marker files
// (__init__.py, .gitkeep) has nothing to do. Asking to approve deleting zero files would
// read stdin for nothing, and declining it would report failure to a script.
if (plan.FileCount == 0)
{
Console.WriteLine("Nothing to delete (only empty-file duplicates found).");
return;
}

Console.WriteLine($"Found {duplicates.Count} group(s) of duplicate files.");
Console.WriteLine("Keeping the copy with the shortest filename in each group.");
Console.WriteLine();
Expand Down
12 changes: 10 additions & 2 deletions FileDeduplicator/Verbs/Scan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ internal override void Run(Scan options)

// Step 4: Display results
long totalWastedBytes = 0;
bool anyDeletable = false;
Console.WriteLine($"Found {duplicates.Count} group(s) of duplicate files:");
Console.WriteLine();

foreach (DuplicateGroup group in duplicates)
{
AbsoluteFilePath keeper = Deduplicator.SelectFileToKeep(group.Files);
bool deletable = Deduplicator.IsDeletable(group);
anyDeletable |= deletable;
long wastedBytes = group.FileSize * (group.Files.Count - 1);
totalWastedBytes += wastedBytes;

Expand All @@ -78,7 +80,13 @@ internal override void Run(Scan options)

Console.WriteLine($"Total duplicate groups: {duplicates.Count}");
Console.WriteLine($"Total wasted space: {DuplicateReport.FormatBytes(totalWastedBytes)}");
Console.WriteLine();
Console.WriteLine("Run the 'Deduplicate' command to remove duplicates.");

// Empty duplicates are kept, so pointing at Deduplicate when they are all there is would
// send the user to a verb with nothing to do.
if (anyDeletable)
{
Console.WriteLine();
Console.WriteLine("Run the 'Deduplicate' command to remove duplicates.");
}
}
}
Loading