Files
Explorer-Workbench/tests/Explorer.Application.Tests/FileOperationProfilePlannerTests.cs

236 lines
8.4 KiB
C#

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));
}
[Fact]
public void Convert_lists_one_job_per_matching_file()
{
var fs = Tree()
.Dir(@"C:\src")
.File(@"C:\src\clip.mov", 10, T0)
.File(@"C:\src\notes.txt", 1, T0)
.Dir(@"C:\dst");
var plan = Build(new OperationProfile
{
Name = "Convert",
DestPath = @"C:\dst",
DoConvert = true,
ConversionKind = ConversionKind.VideoToMp4
}, fs, [@"C:\src"]);
Assert.True(plan.CanEnqueue);
var convert = Assert.Single(plan.Operations);
Assert.Equal(TransferOp.Convert, convert.Op);
Assert.Equal(@"C:\src\clip.mov", convert.SourcePath);
Assert.Equal(@"C:\dst\clip.mp4", convert.DestinationPath);
Assert.Contains(plan.ProfilePreview, r => r.Action == "Convert");
}
[Fact]
public void Missing_ffmpeg_is_an_error()
{
var fs = Tree().Dir(@"C:\src").File(@"C:\src\clip.mov", 10, T0).Dir(@"C:\dst");
var plan = Build(new OperationProfile
{
Name = "Convert",
DestPath = @"C:\dst",
DoConvert = true
}, fs, [@"C:\src"], convertAvailable: false);
Assert.False(plan.CanEnqueue);
Assert.Contains(plan.Issues, i => i.Message.Contains("FFmpeg", 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,
bool convertAvailable = 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,
convertAvailable: convertAvailable,
convertMissingHint: FfmpegLocator.MissingHint);
private static TreeEnumerator Tree() => new();
}