Add queued FFmpeg conversion and finish splitting the window from the host.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
109
tests/Explorer.Application.Tests/ConversionPlannerTests.cs
Normal file
109
tests/Explorer.Application.Tests/ConversionPlannerTests.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
namespace Explorer.Application.Tests;
|
||||
|
||||
public class ConversionPlannerTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = DateTimeOffset.Parse("2024-06-01T12:00:00Z");
|
||||
|
||||
[Fact]
|
||||
public void Video_files_become_mp4_outputs()
|
||||
{
|
||||
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([@"C:\src"], @"C:\dst", ConversionKind.VideoToMp4, fs);
|
||||
Assert.True(plan.CanEnqueue);
|
||||
var op = Assert.Single(plan.Operations);
|
||||
Assert.Equal(TransferOp.Convert, op.Op);
|
||||
Assert.Equal(@"C:\src\clip.mov", op.SourcePath);
|
||||
Assert.Equal(@"C:\dst\clip.mp4", op.DestinationPath);
|
||||
Assert.Contains(plan.Issues, i => i.Message.Contains("not a match", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Online_only_files_are_skipped()
|
||||
{
|
||||
var fs = Tree()
|
||||
.Dir(@"C:\src")
|
||||
.File(@"C:\src\clip.mov", 10, T0)
|
||||
.Dir(@"C:\dst");
|
||||
|
||||
var plan = Build(
|
||||
[@"C:\src\clip.mov"],
|
||||
@"C:\dst",
|
||||
ConversionKind.VideoToMp4,
|
||||
fs,
|
||||
wouldHydrate: item => item.Name == "clip.mov");
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Issues, i => i.Message.Contains("Online-only", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[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([@"C:\src\clip.mov"], @"C:\dst", ConversionKind.VideoToMp4, fs, ffmpegAvailable: false);
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Issues, i => i.Message.Contains("FFmpeg", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unique_names_avoid_overwrite()
|
||||
{
|
||||
var fs = Tree()
|
||||
.Dir(@"C:\src")
|
||||
.File(@"C:\src\clip.mov", 10, T0)
|
||||
.File(@"C:\src\clip.mkv", 10, T0)
|
||||
.Dir(@"C:\dst");
|
||||
|
||||
var plan = Build(
|
||||
[@"C:\src"],
|
||||
@"C:\dst",
|
||||
ConversionKind.VideoToMp4,
|
||||
fs,
|
||||
pathExists: path => path.Equals(@"C:\dst\clip.mp4", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.True(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Operations, o => o.DestinationPath == @"C:\dst\clip (2).mp4");
|
||||
Assert.Contains(plan.Operations, o => o.DestinationPath == @"C:\dst\clip (3).mp4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Heic_maps_to_jpeg()
|
||||
{
|
||||
var fs = Tree()
|
||||
.Dir(@"C:\src")
|
||||
.File(@"C:\src\IMG_0001.HEIC", 4, T0)
|
||||
.Dir(@"C:\dst");
|
||||
|
||||
var plan = Build([@"C:\src\IMG_0001.HEIC"], @"C:\dst", ConversionKind.HeicToJpeg, fs);
|
||||
var op = Assert.Single(plan.Operations);
|
||||
Assert.Equal(@"C:\dst\IMG_0001.jpg", op.DestinationPath);
|
||||
Assert.Equal(nameof(ConversionKind.HeicToJpeg), op.NewName);
|
||||
}
|
||||
|
||||
private static OperationPlan Build(
|
||||
string[] sources,
|
||||
string dest,
|
||||
ConversionKind kind,
|
||||
IFileSystemEnumerator fs,
|
||||
bool ffmpegAvailable = true,
|
||||
Func<string, bool>? pathExists = null,
|
||||
Func<FileSystemItem, bool>? wouldHydrate = null)
|
||||
=> new ConversionPlanner().Build(
|
||||
sources,
|
||||
dest,
|
||||
kind,
|
||||
fs,
|
||||
ffmpegAvailable,
|
||||
FfmpegLocator.MissingHint,
|
||||
pathExists ?? (_ => false),
|
||||
wouldHydrate);
|
||||
|
||||
private static TreeEnumerator Tree() => new();
|
||||
}
|
||||
30
tests/Explorer.Application.Tests/FfmpegLocatorTests.cs
Normal file
30
tests/Explorer.Application.Tests/FfmpegLocatorTests.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Explorer.Application;
|
||||
|
||||
namespace Explorer.Application.Tests;
|
||||
|
||||
public class FfmpegLocatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Prefers_the_configured_path_when_it_exists()
|
||||
{
|
||||
var path = @"C:\Tools\ffmpeg.exe";
|
||||
Assert.Equal(path, FfmpegLocator.Find(path, fileExists: p => p == path, pathVariable: ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Finds_ffmpeg_on_PATH_when_not_configured()
|
||||
{
|
||||
var found = FfmpegLocator.Find(
|
||||
null,
|
||||
fileExists: p => p.Equals(@"D:\bin\ffmpeg.exe", StringComparison.OrdinalIgnoreCase),
|
||||
pathVariable: @"C:\Windows;D:\bin");
|
||||
Assert.Equal(@"D:\bin\ffmpeg.exe", found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Returns_null_when_ffmpeg_is_missing()
|
||||
{
|
||||
Assert.Null(FfmpegLocator.Find(null, fileExists: _ => false, pathVariable: @"C:\none"));
|
||||
Assert.Contains("FFmpeg", FfmpegLocator.MissingHint, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -160,6 +160,44 @@ public class FileOperationProfilePlannerTests
|
||||
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()
|
||||
{
|
||||
@@ -176,6 +214,7 @@ public class FileOperationProfilePlannerTests
|
||||
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(
|
||||
@@ -188,7 +227,9 @@ public class FileOperationProfilePlannerTests
|
||||
compressAvailable,
|
||||
SevenZipLocator.MissingHint,
|
||||
pathExists: _ => false,
|
||||
wouldHydrate: wouldHydrate);
|
||||
wouldHydrate: wouldHydrate,
|
||||
convertAvailable: convertAvailable,
|
||||
convertMissingHint: FfmpegLocator.MissingHint);
|
||||
|
||||
private static TreeEnumerator Tree() => new();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public class UiPreferencesStoreTests
|
||||
Assert.True(prefs.AutoClearQueueWhenDone);
|
||||
Assert.Equal(@"C:\Program Files\7-Zip\7z.exe", prefs.SevenZipPath);
|
||||
Assert.Null(prefs.GitPath);
|
||||
Assert.Null(prefs.FfmpegPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -43,6 +44,13 @@ public class UiPreferencesStoreTests
|
||||
Assert.Equal(@"C:\Program Files\Git\cmd\git.exe", prefs.GitPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_reads_ffmpeg_path()
|
||||
{
|
||||
var prefs = UiPreferencesStore.Parse(["ffmpeg=C:\\Tools\\ffmpeg.exe"]);
|
||||
Assert.Equal(@"C:\Tools\ffmpeg.exe", prefs.FfmpegPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_reads_host_and_removable_index_flags()
|
||||
{
|
||||
@@ -70,6 +78,8 @@ public class UiPreferencesStoreTests
|
||||
Assert.False(prefs.BackgroundHostAtLogon);
|
||||
Assert.Null(prefs.SevenZipPath);
|
||||
Assert.Null(prefs.GitPath);
|
||||
Assert.Null(prefs.FfmpegPath);
|
||||
Assert.True(prefs.SessionTabs is null || prefs.SessionTabs.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -91,6 +101,43 @@ public class UiPreferencesStoreTests
|
||||
Assert.Equal(24, prefs.WindowTop);
|
||||
Assert.True(prefs.WindowMaximized);
|
||||
Assert.Equal(320, prefs.TreeWidth);
|
||||
Assert.True(prefs.SessionTabs is null || prefs.SessionTabs.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_reads_session_tabs_and_active_index()
|
||||
{
|
||||
var left = @"C:\Users\Dominique\Documents";
|
||||
var right = @"D:\Photos";
|
||||
var prefs = UiPreferencesStore.Parse(
|
||||
[
|
||||
"session-active-tab=1",
|
||||
"session-tab=" + UiPreferencesStore.FormatSessionTab(new SessionTabState(LocationRoots.ThisPc)),
|
||||
"session-tab=" + UiPreferencesStore.FormatSessionTab(new SessionTabState(left, right, true, 0.42, true))
|
||||
]);
|
||||
Assert.Equal(1, prefs.SessionActiveTab);
|
||||
Assert.NotNull(prefs.SessionTabs);
|
||||
Assert.Equal(2, prefs.SessionTabs.Count);
|
||||
Assert.Equal(LocationRoots.ThisPc, prefs.SessionTabs[0].LeftPath);
|
||||
Assert.False(prefs.SessionTabs[0].IsSplit);
|
||||
Assert.Equal(left, prefs.SessionTabs[1].LeftPath);
|
||||
Assert.Equal(right, prefs.SessionTabs[1].RightPath);
|
||||
Assert.True(prefs.SessionTabs[1].IsSplit);
|
||||
Assert.Equal(0.42, prefs.SessionTabs[1].SplitRatio);
|
||||
Assert.True(prefs.SessionTabs[1].ActiveIsRight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Session_tab_roundtrip_escapes_semicolons_in_paths()
|
||||
{
|
||||
var state = new SessionTabState(@"C:\weird;name", @"\\server\share", true, 0.3, false);
|
||||
var parsed = UiPreferencesStore.TryParseSessionTab(UiPreferencesStore.FormatSessionTab(state));
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(state.LeftPath, parsed.LeftPath);
|
||||
Assert.Equal(state.RightPath, parsed.RightPath);
|
||||
Assert.True(parsed.IsSplit);
|
||||
Assert.Equal(0.3, parsed.SplitRatio);
|
||||
Assert.False(parsed.ActiveIsRight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -100,7 +147,17 @@ public class UiPreferencesStoreTests
|
||||
try
|
||||
{
|
||||
var store = new UiPreferencesStore(new PrefsEnv(dir));
|
||||
store.Save(new UiPreferences("Light", true, false, true, AutoClearQueueWhenDone: true, WindowWidth: 1100, WindowHeight: 720, TreeWidth: 300));
|
||||
store.Save(new UiPreferences(
|
||||
"Light", true, false, true,
|
||||
AutoClearQueueWhenDone: true,
|
||||
WindowWidth: 1100,
|
||||
WindowHeight: 720,
|
||||
TreeWidth: 300,
|
||||
SessionTabs:
|
||||
[
|
||||
new SessionTabState(@"C:\Temp", @"D:\", true, 0.6, true)
|
||||
],
|
||||
SessionActiveTab: 0));
|
||||
var loaded = store.Load();
|
||||
Assert.Equal("Light", loaded.Theme);
|
||||
Assert.True(loaded.GroupNetworkPlaces);
|
||||
@@ -114,6 +171,13 @@ public class UiPreferencesStoreTests
|
||||
Assert.Equal(1100, loaded.WindowWidth);
|
||||
Assert.Equal(720, loaded.WindowHeight);
|
||||
Assert.Equal(300, loaded.TreeWidth);
|
||||
Assert.NotNull(loaded.SessionTabs);
|
||||
var tab = Assert.Single(loaded.SessionTabs);
|
||||
Assert.Equal(@"C:\Temp", tab.LeftPath);
|
||||
Assert.Equal(@"D:\", tab.RightPath);
|
||||
Assert.True(tab.IsSplit);
|
||||
Assert.Equal(0.6, tab.SplitRatio);
|
||||
Assert.True(tab.ActiveIsRight);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user