238 lines
8.6 KiB
C#
238 lines
8.6 KiB
C#
using Explorer.Application;
|
|
using Explorer.Domain;
|
|
|
|
namespace Explorer.Application.Tests;
|
|
|
|
public class RenamePlannerTests
|
|
{
|
|
private static RenameSubject File(string path)
|
|
=> new(path, PathRules.GetFileName(path), false);
|
|
|
|
[Fact]
|
|
public void Search_replace_prefix_suffix_and_counter()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\a\Vacation.jpg"), File(@"C:\a\Vacation (2).jpg")],
|
|
new RenameRuleSet
|
|
{
|
|
Search = "Vacation",
|
|
Replace = "Trip",
|
|
Prefix = "2024_",
|
|
Suffix = "_ok",
|
|
UseCounter = true,
|
|
CounterStart = 1,
|
|
CounterPadding = 2
|
|
});
|
|
Assert.True(plan.CanEnqueue);
|
|
Assert.Equal("2024_Trip_ok01.jpg", plan.Preview[0].NewName);
|
|
Assert.Equal("2024_Trip (2)_ok02.jpg", plan.Preview[1].NewName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Regex_replace_and_case_and_extension()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\a\IMG_001.JPG")],
|
|
new RenameRuleSet
|
|
{
|
|
Search = @"IMG_(\d+)",
|
|
Replace = "Photo_$1",
|
|
UseRegex = true,
|
|
CaseMode = RenameCaseMode.Lower,
|
|
ChangeExtension = true,
|
|
NewExtension = "jpg"
|
|
});
|
|
Assert.Equal("photo_001.jpg", plan.Preview[0].NewName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Counter_placeholder_does_not_need_hydration()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\a\clip.mp4")],
|
|
new RenameRuleSet { Prefix = "Clip_{Counter}_", UseCounter = true, CounterPadding = 3 });
|
|
Assert.Equal("Clip_001_clip.mp4", plan.Preview[0].NewName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Media_placeholders_use_supplied_tags()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\a\clip.mp4")],
|
|
new RenameRuleSet { Prefix = "{Width}_", NamePattern = "{Artist} - {Title}" },
|
|
tagsByPath: new Dictionary<string, MediaTagFields>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
[@"C:\a\clip.mp4"] = new()
|
|
{
|
|
Width = 1920,
|
|
Artist = "Queen",
|
|
Title = "Bohemian Rhapsody",
|
|
Stem = "clip",
|
|
Extension = "mp4"
|
|
}
|
|
});
|
|
Assert.Equal("1920_Queen - Bohemian Rhapsody.mp4", plan.Preview[0].NewName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Online_only_media_placeholder_is_an_error()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\cloud\clip.mp4")],
|
|
new RenameRuleSet { Prefix = "{Artist}_" },
|
|
tagsByPath: new Dictionary<string, MediaTagFields>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
[@"C:\cloud\clip.mp4"] = new() { HydrationBlocked = true, Stem = "clip", Extension = "mp4" }
|
|
});
|
|
Assert.False(plan.CanEnqueue);
|
|
Assert.Contains(plan.Issues, i => i.Message.Contains("online-only", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void Project_and_taken_placeholders_expand()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\src\Explorer\photos\DSC_001.jpg")],
|
|
new RenameRuleSet { NamePattern = "{Project}_{TakenDate:yyyyMMdd}_{Name}" },
|
|
tagsByPath: new Dictionary<string, MediaTagFields>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
[@"C:\src\Explorer\photos\DSC_001.jpg"] = new()
|
|
{
|
|
Project = "Explorer",
|
|
Taken = new DateTimeOffset(new DateTime(2024, 8, 26, 0, 0, 0, DateTimeKind.Local)),
|
|
Stem = "DSC_001",
|
|
Extension = "jpg"
|
|
}
|
|
});
|
|
Assert.Equal("Explorer_20240826_DSC_001.jpg", plan.Preview[0].NewName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Invalid_windows_name_is_an_error()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\a\notes.txt")],
|
|
new RenameRuleSet { Search = "notes", Replace = "CON" });
|
|
Assert.False(plan.CanEnqueue);
|
|
Assert.Contains(plan.Issues, i => i.Severity == PlanIssueSeverity.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void Collision_when_two_files_map_to_the_same_name()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\a\alpha.txt"), File(@"C:\a\beta.txt")],
|
|
new RenameRuleSet { UseRegex = true, Search = ".+", Replace = "same" });
|
|
Assert.False(plan.CanEnqueue);
|
|
Assert.Contains(plan.Issues, i => i.Message.Contains("same name", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void Existing_path_outside_the_batch_is_a_collision()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\a\one.txt")],
|
|
new RenameRuleSet { Search = "one", Replace = "held" },
|
|
path => path.Equals(@"C:\a\held.txt", StringComparison.OrdinalIgnoreCase));
|
|
Assert.False(plan.CanEnqueue);
|
|
Assert.Contains(plan.Issues, i => i.Message.Contains("already exists", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void Collision_with_an_unchanged_file_in_the_batch()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\a\one.txt"), File(@"C:\a\held.txt")],
|
|
new RenameRuleSet { Search = "one", Replace = "held" });
|
|
Assert.False(plan.CanEnqueue);
|
|
Assert.Contains(plan.Issues, i => i.Message.Contains("already exists", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void Chain_is_ordered_so_the_destination_moves_first()
|
|
{
|
|
var issues = new List<PlanIssue>();
|
|
var ordered = RenamePlanner.Order(
|
|
[
|
|
(@"C:\a\a.txt", @"C:\a\b.txt", "b.txt"),
|
|
(@"C:\a\b.txt", @"C:\a\c.txt", "c.txt")
|
|
],
|
|
issues);
|
|
Assert.Empty(issues);
|
|
Assert.Equal(@"C:\a\b.txt", ordered[0].Source);
|
|
Assert.Equal(@"C:\a\a.txt", ordered[1].Source);
|
|
}
|
|
|
|
[Fact]
|
|
public void Cycle_is_an_error()
|
|
{
|
|
var issues = new List<PlanIssue>();
|
|
RenamePlanner.Order(
|
|
[
|
|
(@"C:\a\x.txt", @"C:\a\y.txt", "y.txt"),
|
|
(@"C:\a\y.txt", @"C:\a\x.txt", "x.txt")
|
|
],
|
|
issues);
|
|
Assert.Contains(issues, i => i.Message.Contains("cycle", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void Undo_restores_names_when_destination_still_matches()
|
|
{
|
|
var batch = new RenameBatch
|
|
{
|
|
Id = 1,
|
|
CreatedUtc = DateTimeOffset.UtcNow,
|
|
Items =
|
|
[
|
|
new RenameBatchItem(@"C:\a\old.txt", @"C:\a\new.txt", 0),
|
|
new RenameBatchItem(@"C:\a\one.txt", @"C:\a\two.txt", 1)
|
|
]
|
|
};
|
|
var exists = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { @"C:\a\new.txt", @"C:\a\two.txt" };
|
|
var plan = new RenamePlanner().BuildUndo(batch, exists.Contains);
|
|
Assert.True(plan.CanEnqueue);
|
|
Assert.Equal(@"C:\a\two.txt", plan.Operations[0].SourcePath);
|
|
Assert.Equal(@"C:\a\old.txt", plan.Operations[1].DestinationPath);
|
|
}
|
|
|
|
[Fact]
|
|
public void Undo_refuses_when_rename_has_not_finished()
|
|
{
|
|
var batch = new RenameBatch
|
|
{
|
|
Id = 1,
|
|
CreatedUtc = DateTimeOffset.UtcNow,
|
|
Items = [new RenameBatchItem(@"C:\a\old.txt", @"C:\a\new.txt", 0)]
|
|
};
|
|
var exists = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { @"C:\a\old.txt" };
|
|
var plan = new RenamePlanner().BuildUndo(batch, exists.Contains);
|
|
Assert.True(plan.HasErrors);
|
|
Assert.Contains(plan.Issues, i => i.Message.Contains("not finished", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void Undo_fails_when_original_name_is_taken()
|
|
{
|
|
var batch = new RenameBatch
|
|
{
|
|
Id = 1,
|
|
CreatedUtc = DateTimeOffset.UtcNow,
|
|
Items = [new RenameBatchItem(@"C:\a\old.txt", @"C:\a\new.txt", 0)]
|
|
};
|
|
var exists = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { @"C:\a\new.txt", @"C:\a\old.txt" };
|
|
var plan = new RenamePlanner().BuildUndo(batch, exists.Contains);
|
|
Assert.True(plan.HasErrors);
|
|
}
|
|
|
|
[Fact]
|
|
public void Invalid_regex_is_an_error()
|
|
{
|
|
var plan = new RenamePlanner().Build(
|
|
[File(@"C:\a\a.txt")],
|
|
new RenameRuleSet { UseRegex = true, Search = "(" });
|
|
Assert.False(plan.CanEnqueue);
|
|
Assert.Contains(plan.Issues, i => i.Message.Contains("regular expression", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
}
|