166 lines
6.4 KiB
C#
166 lines
6.4 KiB
C#
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);
|
|
}
|
|
}
|