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,165 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Explorer.FileOperations;
|
||||
using Explorer.Storage.Sqlite;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Explorer.FileOperations.Tests;
|
||||
|
||||
public class OperationProfileServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Dirty_git_does_not_enqueue()
|
||||
{
|
||||
await using var ctx = await ProfileHarness.CreateAsync();
|
||||
ctx.Git.Status = new GitStatus { RepoRoot = ctx.Root, Branch = "main", ModifiedCount = 2 };
|
||||
ctx.Profile.RequireGitClean = true;
|
||||
var plan = await ctx.Profiles.EnqueueAsync(ctx.Profile);
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Issues, i => i.Message == "Working tree is not clean.");
|
||||
Assert.Empty(ctx.Queue.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Auto_run_fires_only_after_the_destination_returns()
|
||||
{
|
||||
await using var ctx = await ProfileHarness.CreateAsync();
|
||||
ctx.Profile.AutoRun = true;
|
||||
await ctx.Profiles.SaveAsync(ctx.Profile);
|
||||
await ctx.Profiles.TryAutoRunAsync();
|
||||
Assert.Empty(ctx.Queue.Snapshot());
|
||||
|
||||
ctx.Volumes.Reachable = false;
|
||||
await ctx.Profiles.TryAutoRunAsync();
|
||||
Assert.Empty(ctx.Queue.Snapshot());
|
||||
|
||||
ctx.Volumes.Reachable = true;
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Profiles.TryAutoRunAsync();
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Any(j => j.Status == TransferStatus.Done));
|
||||
Assert.True(File.Exists(Path.Combine(ctx.Dest, Path.GetFileName(ctx.Root), "a.txt")));
|
||||
|
||||
File.WriteAllText(Path.Combine(ctx.Root, "b.txt"), "b");
|
||||
await ctx.Profiles.TryAutoRunAsync();
|
||||
Assert.DoesNotContain(ctx.Queue.Snapshot(), j => j.SourcePath.EndsWith("b.txt", StringComparison.OrdinalIgnoreCase));
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Compress_auto_run_is_ignored()
|
||||
{
|
||||
await using var ctx = await ProfileHarness.CreateAsync();
|
||||
ctx.Profile.DoCompress = true;
|
||||
ctx.Profile.AutoRun = true;
|
||||
await ctx.Profiles.SaveAsync(ctx.Profile);
|
||||
Assert.False((await ctx.Store.OperationProfiles.GetAsync(ctx.Profile.Id))!.CanAutoRun);
|
||||
ctx.Volumes.Reachable = false;
|
||||
await ctx.Profiles.TryAutoRunAsync();
|
||||
ctx.Volumes.Reachable = true;
|
||||
await ctx.Profiles.TryAutoRunAsync();
|
||||
Assert.Empty(ctx.Queue.Snapshot());
|
||||
}
|
||||
|
||||
private static async Task WaitUntil(Func<bool> condition)
|
||||
{
|
||||
var limit = DateTime.UtcNow + TimeSpan.FromSeconds(4);
|
||||
while (!condition())
|
||||
{
|
||||
if (DateTime.UtcNow > limit)
|
||||
{
|
||||
throw new TimeoutException("Condition was not met.");
|
||||
}
|
||||
|
||||
await Task.Delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ProfileHarness : IAsyncDisposable
|
||||
{
|
||||
public required OperationProfileService Profiles { get; init; }
|
||||
public required TransferQueue Queue { get; init; }
|
||||
public required ControlledVolumes Volumes { get; init; }
|
||||
public required SqliteIndexStore Store { get; init; }
|
||||
public required OperationProfile Profile { get; init; }
|
||||
public required StubGit Git { get; init; }
|
||||
public required string Root { get; init; }
|
||||
public required string Dest { get; init; }
|
||||
|
||||
public static async Task<ProfileHarness> CreateAsync()
|
||||
{
|
||||
var work = Path.Combine(Path.GetTempPath(), "ew-op", Guid.NewGuid().ToString("N"));
|
||||
var source = Path.Combine(work, "src");
|
||||
var dest = Path.Combine(work, "dest");
|
||||
Directory.CreateDirectory(source);
|
||||
Directory.CreateDirectory(dest);
|
||||
File.WriteAllText(Path.Combine(source, "a.txt"), "a");
|
||||
var store = new SqliteIndexStore(Path.Combine(work, "index.db"), NullLogger<SqliteIndexStore>.Instance);
|
||||
await store.OpenAsync();
|
||||
var shell = new GateShell();
|
||||
var volumes = new ControlledVolumes();
|
||||
var enumerator = new DiskEnum();
|
||||
var queue = new TransferQueue(
|
||||
new NativeFileOperationExecutor(shell, enumerator),
|
||||
store,
|
||||
volumes,
|
||||
NullLogger<TransferQueue>.Instance);
|
||||
var ops = new FileOperationService(queue, shell, enumerator);
|
||||
var git = new StubGit();
|
||||
var planner = new FileOperationProfilePlanner(new RenamePlanner());
|
||||
var renames = new RenameBatchService(new RenamePlanner(), store, ops);
|
||||
var profiles = new OperationProfileService(
|
||||
planner,
|
||||
store,
|
||||
ops,
|
||||
renames,
|
||||
volumes,
|
||||
enumerator,
|
||||
git,
|
||||
new NeverHydrate(),
|
||||
new FakeArchiveExecutor());
|
||||
return new ProfileHarness
|
||||
{
|
||||
Profiles = profiles,
|
||||
Queue = queue,
|
||||
Volumes = volumes,
|
||||
Store = store,
|
||||
Git = git,
|
||||
Profile = new OperationProfile
|
||||
{
|
||||
Name = "Copy",
|
||||
SourcePath = source,
|
||||
DestPath = dest,
|
||||
DoCopy = true
|
||||
},
|
||||
Root = source,
|
||||
Dest = dest
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Store.DisposeAsync();
|
||||
var work = Path.GetDirectoryName(Root);
|
||||
try { if (work is not null) Directory.Delete(work, true); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubGit : IGitStatusProvider
|
||||
{
|
||||
public bool IsAvailable { get; set; } = true;
|
||||
public GitStatus? Status { get; set; }
|
||||
public string? FindRepoRoot(string path) => Status?.RepoRoot;
|
||||
public bool IsRepoRoot(string path) => Status is not null;
|
||||
public Task<GitStatus?> GetStatusAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(Status);
|
||||
}
|
||||
|
||||
private sealed class NeverHydrate : IHydrationGuard
|
||||
{
|
||||
public bool WouldHydrateOnRead(FileSystemItem item) => false;
|
||||
public bool WouldHydrateOnRead(int attributes, CloudAvailability? availability) => false;
|
||||
public Task<bool> WouldHydrateOnReadAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user