Add Git overlay, operation tools, and virtualized preview so large folders stay responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
namespace Explorer.Application.Tests;
|
||||
|
||||
public class FileOperationProfilePlannerTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = DateTimeOffset.Parse("2024-06-01T12:00:00Z");
|
||||
|
||||
[Fact]
|
||||
public void Copy_lists_copy_operations_only()
|
||||
{
|
||||
var fs = Tree()
|
||||
.Dir(@"C:\src")
|
||||
.File(@"C:\src\a.txt", 1, T0)
|
||||
.File(@"C:\src\b.txt", 2, T0)
|
||||
.Dir(@"C:\dst");
|
||||
|
||||
var plan = Build(CopyProfile(), fs, [@"C:\src"]);
|
||||
Assert.True(plan.CanEnqueue);
|
||||
Assert.All(plan.Operations, o => Assert.Equal(TransferOp.Copy, o.Op));
|
||||
Assert.Contains(plan.Operations, o => o.SourcePath == @"C:\src\a.txt" && o.DestinationPath == @"C:\dst\src\a.txt");
|
||||
Assert.Contains(plan.Operations, o => o.SourcePath == @"C:\src\b.txt" && o.DestinationPath == @"C:\dst\src\b.txt");
|
||||
Assert.Contains(plan.ProfilePreview, r => r.Action == "Copy" && r.Path == @"C:\dst\src\a.txt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compress_lists_one_archive_job()
|
||||
{
|
||||
var fs = Tree()
|
||||
.Dir(@"C:\src")
|
||||
.File(@"C:\src\a.txt", 1, T0)
|
||||
.File(@"C:\src\b.txt", 2, T0)
|
||||
.Dir(@"C:\dst");
|
||||
|
||||
var plan = Build(new OperationProfile
|
||||
{
|
||||
Name = "Archive",
|
||||
DestPath = @"C:\dst",
|
||||
DoCompress = true
|
||||
}, fs, [@"C:\src"]);
|
||||
Assert.True(plan.CanEnqueue);
|
||||
var compress = Assert.Single(plan.Operations);
|
||||
Assert.Equal(TransferOp.Compress, compress.Op);
|
||||
Assert.Equal(@"C:\src\a.txt|C:\src\b.txt", compress.SourcePath);
|
||||
Assert.Equal(@"C:\dst\src.7z", compress.DestinationPath);
|
||||
Assert.Contains(plan.ProfilePreview, r => r.Action == "Compress");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dirty_git_stops_before_operations()
|
||||
{
|
||||
var fs = Tree()
|
||||
.Dir(@"C:\src")
|
||||
.File(@"C:\src\a.txt", 1, T0)
|
||||
.Dir(@"C:\dst");
|
||||
var git = new GitStatus { RepoRoot = @"C:\src", Branch = "main", ModifiedCount = 1 };
|
||||
|
||||
var plan = Build(CopyProfile(git: true), fs, [@"C:\src"], git);
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Empty(plan.Operations);
|
||||
Assert.Contains(plan.Issues, i => i.Message == "Working tree is not clean.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Missing_git_and_missing_repo_are_errors()
|
||||
{
|
||||
var fs = Tree().Dir(@"C:\src").File(@"C:\src\a.txt", 1, T0).Dir(@"C:\dst");
|
||||
var noGit = Build(CopyProfile(git: true), fs, [@"C:\src"], gitAvailable: false);
|
||||
Assert.Contains(noGit.Issues, i => i.Message == "git.exe is not available.");
|
||||
|
||||
var noRepo = Build(CopyProfile(git: true), fs, [@"C:\src"], git: null);
|
||||
Assert.Contains(noRepo.Issues, i => i.Message == "Source is not a Git repository.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Offline_destination_is_an_error()
|
||||
{
|
||||
var fs = Tree()
|
||||
.Dir(@"C:\src")
|
||||
.File(@"C:\src\a.txt", 1, T0)
|
||||
.Dir(@"Z:\backup");
|
||||
|
||||
var plan = Build(
|
||||
CopyProfile(@"Z:\backup"),
|
||||
fs,
|
||||
[@"C:\src"],
|
||||
reachable: path => path.StartsWith(@"C:\", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Empty(plan.Operations);
|
||||
Assert.Contains(plan.Issues, i => i.Message == "Destination is not available.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rename_then_copy_uses_new_names()
|
||||
{
|
||||
var fs = Tree()
|
||||
.Dir(@"C:\src")
|
||||
.File(@"C:\src\keep.txt", 1, T0)
|
||||
.File(@"C:\src\a.txt", 1, T0)
|
||||
.Dir(@"C:\dst");
|
||||
|
||||
var plan = Build(new OperationProfile
|
||||
{
|
||||
Name = "Rename copy",
|
||||
DestPath = @"C:\dst",
|
||||
DoCopy = true,
|
||||
DoRename = true,
|
||||
RenamePrefix = "x_"
|
||||
}, fs, [@"C:\src"]);
|
||||
Assert.True(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Operations, o => o.Op == TransferOp.Rename && o.NewName == "x_keep.txt");
|
||||
Assert.Contains(plan.Operations, o =>
|
||||
o.Op == TransferOp.Copy
|
||||
&& o.SourcePath == @"C:\src\x_a.txt"
|
||||
&& o.DestinationPath == @"C:\dst\src\x_a.txt");
|
||||
Assert.Contains(plan.Operations, o =>
|
||||
o.Op == TransferOp.Copy
|
||||
&& o.SourcePath == @"C:\src\x_keep.txt"
|
||||
&& o.DestinationPath == @"C:\dst\src\x_keep.txt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Excludes_and_online_only_files_are_skipped()
|
||||
{
|
||||
var fs = Tree()
|
||||
.Dir(@"C:\src")
|
||||
.File(@"C:\src\keep.txt", 1, T0)
|
||||
.File(@"C:\src\skip.tmp", 1, T0)
|
||||
.Dir(@"C:\src\bin")
|
||||
.File(@"C:\src\cloud.bin", 1, T0)
|
||||
.Dir(@"C:\dst");
|
||||
|
||||
var plan = Build(new OperationProfile
|
||||
{
|
||||
Name = "Copy",
|
||||
DestPath = @"C:\dst",
|
||||
DoCopy = true,
|
||||
Excludes = "*.tmp\nbin"
|
||||
}, fs, [@"C:\src"], wouldHydrate: item => item.Name == "cloud.bin");
|
||||
Assert.Contains(plan.Operations, o => o.SourcePath.EndsWith("keep.txt", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(plan.Operations, o => o.SourcePath.EndsWith("skip.tmp", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(plan.Operations, o => o.SourcePath.Contains(@"\bin", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(plan.Operations, o => o.SourcePath.EndsWith("cloud.bin", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(plan.Issues, i => i.Message.Contains("Online-only", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Missing_7zip_is_an_error()
|
||||
{
|
||||
var fs = Tree().Dir(@"C:\src").File(@"C:\src\a.txt", 1, T0).Dir(@"C:\dst");
|
||||
var plan = Build(new OperationProfile
|
||||
{
|
||||
Name = "Archive",
|
||||
DestPath = @"C:\dst",
|
||||
DoCompress = true
|
||||
}, fs, [@"C:\src"], compressAvailable: false);
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Issues, i => i.Message.Contains("7-Zip", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static OperationProfile CopyProfile(string dest = @"C:\dst", bool git = false)
|
||||
=> new()
|
||||
{
|
||||
Name = "Copy",
|
||||
DestPath = dest,
|
||||
DoCopy = true,
|
||||
RequireGitClean = git
|
||||
};
|
||||
|
||||
private static OperationPlan Build(
|
||||
OperationProfile profile,
|
||||
IFileSystemEnumerator fs,
|
||||
string[] sources,
|
||||
GitStatus? git = null,
|
||||
bool gitAvailable = true,
|
||||
bool compressAvailable = true,
|
||||
Func<string, bool>? reachable = null,
|
||||
Func<FileSystemItem, bool>? wouldHydrate = null)
|
||||
=> new FileOperationProfilePlanner(new RenamePlanner()).Build(
|
||||
profile,
|
||||
sources,
|
||||
fs,
|
||||
reachable ?? (_ => true),
|
||||
git,
|
||||
gitAvailable,
|
||||
compressAvailable,
|
||||
SevenZipLocator.MissingHint,
|
||||
pathExists: _ => false,
|
||||
wouldHydrate: wouldHydrate);
|
||||
|
||||
private static TreeEnumerator Tree() => new();
|
||||
}
|
||||
Reference in New Issue
Block a user