165 lines
6.3 KiB
C#
165 lines
6.3 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 FolderSyncServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task Enqueue_copy_waits_when_the_destination_is_offline()
|
|
{
|
|
await using var ctx = await SyncHarness.CreateAsync();
|
|
var plan = await ctx.Sync.PreviewAsync(ctx.Profile);
|
|
Assert.True(plan.CanEnqueue);
|
|
ctx.Volumes.Reachable = false;
|
|
await ctx.Queue.StartAsync(CancellationToken.None);
|
|
await ctx.Sync.EnqueueAsync(ctx.Profile, plan);
|
|
await WaitUntil(() => ctx.Queue.Snapshot().Any(j => j.Status == TransferStatus.Waiting));
|
|
Assert.Equal(0, ctx.Shell.CopyCount);
|
|
ctx.Volumes.Reachable = true;
|
|
ctx.Queue.NotifyAvailability();
|
|
await WaitUntil(() => ctx.Queue.Snapshot().All(j => j.Status == TransferStatus.Done));
|
|
Assert.True(File.Exists(Path.Combine(ctx.Dest, "a.txt")));
|
|
await ctx.Queue.StopAsync(CancellationToken.None);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Auto_run_fires_only_after_the_destination_returns()
|
|
{
|
|
await using var ctx = await SyncHarness.CreateAsync();
|
|
ctx.Profile.AutoRun = true;
|
|
await ctx.Sync.SaveAsync(ctx.Profile);
|
|
await ctx.Sync.TryAutoRunAsync();
|
|
Assert.Empty(ctx.Queue.Snapshot());
|
|
|
|
ctx.Volumes.Reachable = false;
|
|
await ctx.Sync.TryAutoRunAsync();
|
|
Assert.Empty(ctx.Queue.Snapshot());
|
|
|
|
ctx.Volumes.Reachable = true;
|
|
await ctx.Queue.StartAsync(CancellationToken.None);
|
|
await ctx.Sync.TryAutoRunAsync();
|
|
await WaitUntil(() => ctx.Queue.Snapshot().Any(j => j.Status == TransferStatus.Done));
|
|
Assert.True(File.Exists(Path.Combine(ctx.Dest, "a.txt")));
|
|
|
|
File.WriteAllText(Path.Combine(ctx.Root, "b.txt"), "b");
|
|
await ctx.Sync.TryAutoRunAsync();
|
|
Assert.DoesNotContain(ctx.Queue.Snapshot(), j => j.SourcePath.EndsWith("b.txt", StringComparison.OrdinalIgnoreCase));
|
|
await ctx.Queue.StopAsync(CancellationToken.None);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mirror_auto_run_is_ignored()
|
|
{
|
|
await using var ctx = await SyncHarness.CreateAsync();
|
|
ctx.Profile.Mode = SyncMode.Mirror;
|
|
ctx.Profile.AutoRun = true;
|
|
await ctx.Sync.SaveAsync(ctx.Profile);
|
|
ctx.Volumes.Reachable = false;
|
|
await ctx.Sync.TryAutoRunAsync();
|
|
ctx.Volumes.Reachable = true;
|
|
await ctx.Sync.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 SyncHarness : IAsyncDisposable
|
|
{
|
|
public required FolderSyncService Sync { get; init; }
|
|
public required TransferQueue Queue { get; init; }
|
|
public required GateShell Shell { get; init; }
|
|
public required ControlledVolumes Volumes { get; init; }
|
|
public required SqliteIndexStore Store { get; init; }
|
|
public required SyncProfile Profile { get; init; }
|
|
public required string Root { get; init; }
|
|
public required string Dest { get; init; }
|
|
|
|
public static async Task<SyncHarness> CreateAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "ew-sync", Guid.NewGuid().ToString("N"));
|
|
var source = Path.Combine(root, "src");
|
|
var dest = Path.Combine(root, "dest");
|
|
Directory.CreateDirectory(source);
|
|
Directory.CreateDirectory(dest);
|
|
File.WriteAllText(Path.Combine(source, "a.txt"), "a");
|
|
var store = new SqliteIndexStore(Path.Combine(root, "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 env = new SyncEnv(root);
|
|
var sources = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
|
var sync = new FolderSyncService(
|
|
new FolderSyncPlanner(),
|
|
store,
|
|
sources,
|
|
ops,
|
|
volumes,
|
|
enumerator,
|
|
new NeverHydrate());
|
|
return new SyncHarness
|
|
{
|
|
Sync = sync,
|
|
Queue = queue,
|
|
Shell = shell,
|
|
Volumes = volumes,
|
|
Store = store,
|
|
Profile = new SyncProfile
|
|
{
|
|
Name = "Test",
|
|
SourcePath = source,
|
|
DestPath = dest,
|
|
Mode = SyncMode.CopyUpdate
|
|
},
|
|
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 SyncEnv(string dir) : IAppEnvironment
|
|
{
|
|
public string DataDirectory { get; } = dir;
|
|
public string DatabasePath { get; } = Path.Combine(dir, "index.db");
|
|
public string LogDirectory { get; } = Path.Combine(dir, "logs");
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|