Cut over the window to Explorer.Host.exe so only the host writes the index.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,9 +11,13 @@ public class WorkbenchHostTests
|
||||
{
|
||||
var indexing = new FakeIndexing();
|
||||
var transfers = new FakeTransfers();
|
||||
IWorkbenchHost host = new WorkbenchHost(indexing, transfers);
|
||||
var sources = new StubSources();
|
||||
var mutations = new StubMutations();
|
||||
IWorkbenchHost host = new WorkbenchHost(indexing, transfers, sources, mutations);
|
||||
Assert.Same(indexing, host.Indexing);
|
||||
Assert.Same(transfers, host.Transfers);
|
||||
Assert.Same(sources, host.Sources);
|
||||
Assert.Same(mutations, host.Mutations);
|
||||
|
||||
host.Indexing.EnqueueFullScan(7);
|
||||
host.Transfers.PauseAll();
|
||||
@@ -57,4 +61,30 @@ public class WorkbenchHostTests
|
||||
public bool MoveUp(long jobId) => false;
|
||||
public bool MoveDown(long jobId) => false;
|
||||
}
|
||||
|
||||
private sealed class StubSources : ISourceHost
|
||||
{
|
||||
public Task RefreshAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<Source> AddUncAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new Source { StableKey = "x", DisplayName = path });
|
||||
public Task<Source?> EnsureForPathAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<Source?>(null);
|
||||
public Task<bool> ForgetAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(false);
|
||||
}
|
||||
|
||||
private sealed class StubMutations : IIndexMutations
|
||||
{
|
||||
public Task<long> UpsertSyncProfileAsync(SyncProfile profile, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(0L);
|
||||
public Task DeleteSyncProfileAsync(long id, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<long> UpsertOperationProfileAsync(OperationProfile profile, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(0L);
|
||||
public Task DeleteOperationProfileAsync(long id, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<long> CreateRenameBatchAsync(IReadOnlyList<RenameBatchItem> items, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(0L);
|
||||
public Task MarkRenameBatchUndoneAsync(long id, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task EnqueueHashCollisionsAsync(long? sourceId, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task UpsertRelationAsync(FileRelation relation, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,12 +109,14 @@ public class FolderSyncServiceTests
|
||||
store,
|
||||
volumes,
|
||||
NullLogger<TransferQueue>.Instance);
|
||||
var mutations = new LocalIndexMutations(store);
|
||||
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,
|
||||
mutations,
|
||||
sources,
|
||||
ops,
|
||||
volumes,
|
||||
|
||||
@@ -107,10 +107,12 @@ public class OperationProfileServiceTests
|
||||
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 mutations = new LocalIndexMutations(store);
|
||||
var renames = new RenameBatchService(new RenamePlanner(), store, mutations, ops);
|
||||
var profiles = new OperationProfileService(
|
||||
planner,
|
||||
store,
|
||||
mutations,
|
||||
ops,
|
||||
renames,
|
||||
volumes,
|
||||
|
||||
@@ -316,7 +316,7 @@ public class TransferQueueTests
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
|
||||
var batches = new RenameBatchService(new RenamePlanner(), ctx.Store, ops);
|
||||
var batches = new RenameBatchService(new RenamePlanner(), ctx.Store, new LocalIndexMutations(ctx.Store), ops);
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
var subjects = new[]
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Explorer.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Explorer.Hosting.Tests;
|
||||
|
||||
@@ -23,6 +24,7 @@ public class CoreRegistrationTests
|
||||
Assert.NotNull(sp.GetService<IWorkbenchHost>());
|
||||
Assert.NotNull(sp.GetService<IIndexingHost>());
|
||||
Assert.NotNull(sp.GetService<ITransferHost>());
|
||||
Assert.NotNull(sp.GetService<IIndexMutations>());
|
||||
Assert.Null(sp.GetService<IOsClipboard>());
|
||||
}
|
||||
finally
|
||||
@@ -31,6 +33,35 @@ public class CoreRegistrationTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddExplorerClient_uses_read_only_store_without_indexing_workers()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "ew-hosting", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
try
|
||||
{
|
||||
var workbench = new StubWorkbench();
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddSingleton<IAppEnvironment>(new TempEnv(dir));
|
||||
services.AddExplorerClient(workbench);
|
||||
await using var sp = services.BuildServiceProvider();
|
||||
Assert.Same(workbench, sp.GetService<IWorkbenchHost>());
|
||||
Assert.Same(workbench.Indexing, sp.GetService<IIndexingHost>());
|
||||
Assert.Same(workbench.Transfers, sp.GetService<ITransferHost>());
|
||||
Assert.Same(workbench.Sources, sp.GetService<ISourceHost>());
|
||||
Assert.Same(workbench.Mutations, sp.GetService<IIndexMutations>());
|
||||
Assert.False(sp.GetRequiredService<IIndexStore>().CanWrite);
|
||||
var hosted = sp.GetServices<IHostedService>().ToList();
|
||||
Assert.Contains(hosted, s => s is IndexStoreLifetime);
|
||||
Assert.DoesNotContain(hosted, s => s.GetType().Name is "IndexingCoordinator" or "TransferQueue" or "WatcherHostedService");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(dir, true); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TempEnv : IAppEnvironment
|
||||
{
|
||||
public TempEnv(string dir)
|
||||
@@ -45,4 +76,65 @@ public class CoreRegistrationTests
|
||||
public string DatabasePath { get; }
|
||||
public string LogDirectory { get; }
|
||||
}
|
||||
|
||||
private sealed class StubWorkbench : IWorkbenchHost
|
||||
{
|
||||
public IIndexingHost Indexing { get; } = new StubIndexing();
|
||||
public ITransferHost Transfers { get; } = new StubTransfers();
|
||||
public ISourceHost Sources { get; } = new StubSources();
|
||||
public IIndexMutations Mutations { get; } = new StubMutations();
|
||||
}
|
||||
|
||||
private sealed class StubIndexing : IIndexingHost
|
||||
{
|
||||
public event EventHandler<ScanProgress>? ProgressChanged = delegate { };
|
||||
public void EnqueueFullScan(long sourceId) { }
|
||||
public void EnqueueFolderScan(long sourceId, string pathRel) { }
|
||||
public void EnqueueReconcile(long sourceId, string pathRel) { }
|
||||
public void Cancel(long sourceId) { }
|
||||
}
|
||||
|
||||
private sealed class StubTransfers : ITransferHost
|
||||
{
|
||||
public event EventHandler? Changed = delegate { };
|
||||
public event EventHandler<TransferJob>? JobFinished = delegate { };
|
||||
public bool IsPaused => false;
|
||||
public IReadOnlyList<TransferJob> Snapshot() => [];
|
||||
public void PauseAll() { }
|
||||
public void ResumeAll() { }
|
||||
public void Pause(long jobId) { }
|
||||
public void Resume(long jobId) { }
|
||||
public void Retry(long jobId) { }
|
||||
public void Cancel(long jobId) { }
|
||||
public void Dismiss(long jobId) { }
|
||||
public void ClearFinished() { }
|
||||
public bool MoveUp(long jobId) => false;
|
||||
public bool MoveDown(long jobId) => false;
|
||||
}
|
||||
|
||||
private sealed class StubSources : ISourceHost
|
||||
{
|
||||
public Task RefreshAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<Source> AddUncAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new Source { StableKey = "x", DisplayName = path });
|
||||
public Task<Source?> EnsureForPathAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<Source?>(null);
|
||||
public Task<bool> ForgetAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(false);
|
||||
}
|
||||
|
||||
private sealed class StubMutations : IIndexMutations
|
||||
{
|
||||
public Task<long> UpsertSyncProfileAsync(SyncProfile profile, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(0L);
|
||||
public Task DeleteSyncProfileAsync(long id, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<long> UpsertOperationProfileAsync(OperationProfile profile, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(0L);
|
||||
public Task DeleteOperationProfileAsync(long id, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<long> CreateRenameBatchAsync(IReadOnlyList<RenameBatchItem> items, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(0L);
|
||||
public Task MarkRenameBatchUndoneAsync(long id, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task EnqueueHashCollisionsAsync(long? sourceId, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task UpsertRelationAsync(FileRelation relation, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class WorkbenchPipeTests
|
||||
var indexing = new FakeIndexing();
|
||||
var transfers = new FakeTransfers();
|
||||
var server = new WorkbenchPipeServer(
|
||||
new WorkbenchHost(indexing, transfers),
|
||||
new WorkbenchHost(indexing, transfers, new StubSources(), new StubMutations()),
|
||||
new WorkbenchIpcOptions { PipeName = "ew-test" },
|
||||
NullLogger<WorkbenchPipeServer>.Instance);
|
||||
|
||||
@@ -31,13 +31,23 @@ public class WorkbenchPipeTests
|
||||
|
||||
var paused = server.Handle(new IpcEnvelope { V = WorkbenchIpc.ProtocolVersion, Op = "Transfers.IsPaused" });
|
||||
Assert.True(paused.Paused);
|
||||
|
||||
var copy = server.Handle(new IpcEnvelope
|
||||
{
|
||||
V = WorkbenchIpc.ProtocolVersion,
|
||||
Op = "Transfers.EnqueueCopy",
|
||||
Paths = ["C:\\a.txt"],
|
||||
Dest = "D:\\"
|
||||
});
|
||||
Assert.True(copy.Ok);
|
||||
Assert.Equal(@"D:\", transfers.CopiedDest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handle_rejects_other_protocol_versions()
|
||||
{
|
||||
var server = new WorkbenchPipeServer(
|
||||
new WorkbenchHost(new FakeIndexing(), new FakeTransfers()),
|
||||
new WorkbenchHost(new FakeIndexing(), new FakeTransfers(), new StubSources(), new StubMutations()),
|
||||
new WorkbenchIpcOptions(),
|
||||
NullLogger<WorkbenchPipeServer>.Instance);
|
||||
var reply = server.Handle(new IpcEnvelope { V = 99, Op = "Ping" });
|
||||
@@ -72,5 +82,37 @@ public class WorkbenchPipeTests
|
||||
public void ClearFinished() { }
|
||||
public bool MoveUp(long jobId) => false;
|
||||
public bool MoveDown(long jobId) => false;
|
||||
public string? CopiedDest { get; private set; }
|
||||
public Task EnqueueCopyAsync(IReadOnlyList<string> sources, string destinationDirectory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
CopiedDest = destinationDirectory;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubSources : ISourceHost
|
||||
{
|
||||
public Task RefreshAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<Source> AddUncAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new Source { StableKey = "x", DisplayName = path });
|
||||
public Task<Source?> EnsureForPathAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<Source?>(null);
|
||||
public Task<bool> ForgetAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(false);
|
||||
}
|
||||
|
||||
private sealed class StubMutations : IIndexMutations
|
||||
{
|
||||
public Task<long> UpsertSyncProfileAsync(SyncProfile profile, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(1L);
|
||||
public Task DeleteSyncProfileAsync(long id, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<long> UpsertOperationProfileAsync(OperationProfile profile, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(1L);
|
||||
public Task DeleteOperationProfileAsync(long id, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task<long> CreateRenameBatchAsync(IReadOnlyList<RenameBatchItem> items, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(1L);
|
||||
public Task MarkRenameBatchUndoneAsync(long id, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task EnqueueHashCollisionsAsync(long? sourceId, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public Task UpsertRelationAsync(FileRelation relation, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,4 +451,24 @@ public class IndexStoreLockTests
|
||||
Assert.Equal(IndexStoreLock.MutexNameFor(path), IndexStoreLock.MutexNameFor(path));
|
||||
Assert.StartsWith(@"Local\ExplorerWorkbench-Index-", IndexStoreLock.MutexNameFor(path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_only_store_opens_while_writer_holds_the_mutex()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), "ew-tests", Guid.NewGuid().ToString("N"), "index.db");
|
||||
await using var writer = new SqliteIndexStore(path, NullLogger<SqliteIndexStore>.Instance);
|
||||
await writer.OpenAsync();
|
||||
await writer.AddSourceAsync(@"C:\data");
|
||||
|
||||
await using var reader = new SqliteIndexStore(path, NullLogger<SqliteIndexStore>.Instance, readOnly: true);
|
||||
await reader.OpenAsync();
|
||||
Assert.False(reader.CanWrite);
|
||||
var sources = await reader.Sources.GetAllAsync();
|
||||
Assert.Single(sources);
|
||||
Assert.Equal(@"C:\data", sources[0].LastRootPath);
|
||||
|
||||
var write = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => reader.RunWriteAsync(_ => Task.CompletedTask));
|
||||
Assert.Contains("read-only", write.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user