Files
Workspace/Tools/AgentSupport/RuntimeTests/DeferredStorageTests.cs
T

169 lines
7.1 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using NUnit.Framework;
using ShrinkDataSaver;
using ShrinkEventBus;
using ShrinkSDK.Runtime;
namespace Shrink.Inspect.RuntimeTests;
[TestFixture, NonParallelizable]
public sealed class DeferredStorageTests
{
private IShrinkMainThreadDispatcher _previous = null!;
[SetUp]
public void SetUp() => _previous = ShrinkEventBusRuntime.MainThreadDispatcher;
[TearDown]
public void TearDown() => ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(_previous);
[Test]
public void DeferredUsesSeparateDispatcherAndNextPumpForRecursion()
{
var dispatcher = new Dispatcher();
ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher);
using var bus = EventBus.CreateBus(ShrinkBusKey.World(Guid.NewGuid().ToString()),
new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread });
var seen = new List<int>();
using var observer = EventBus.ObserveDiagnostics(trace =>
{
if (trace.BusKey != bus.Key) return;
seen.Add(1);
if (seen.Count == 1) EventBus.PostDeferred(bus, new Evt());
});
Assert.That(EventBus.PostDeferred(bus, new Evt()).Accepted, Is.True);
Assert.That(seen, Is.Empty);
dispatcher.Pump();
Assert.That(seen.Count, Is.EqualTo(1));
dispatcher.Pump();
Assert.That(seen.Count, Is.EqualTo(2));
Assert.That(dispatcher.InlineCalls, Is.Zero);
EventBus.RemoveBus(bus.Key);
}
[Test]
public void OverflowDuringCallbackDoesNotExecuteNewEventsInSamePump()
{
var dispatcher = new Dispatcher();
ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher);
using var bus = EventBus.CreateBus(ShrinkBusKey.World(Guid.NewGuid().ToString()),
new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread,
QueueCapacity = 2, OverflowPolicy = ShrinkQueueOverflowPolicy.DropOldest });
var count = 0;
using var observer = EventBus.ObserveDiagnostics(trace =>
{
if (trace.BusKey != bus.Key) return;
if (++count == 1)
for (var i = 0; i < 3; i++) EventBus.PostDeferred(bus, new Evt());
});
EventBus.PostDeferred(bus, new Evt());
EventBus.PostDeferred(bus, new Evt());
dispatcher.Pump();
Assert.That(count, Is.EqualTo(2));
dispatcher.Pump();
Assert.That(count, Is.EqualTo(4));
EventBus.RemoveBus(bus.Key);
}
[Test]
public void RejectedDispatcherDoesNotConsumeCapacity()
{
var dispatcher = new Dispatcher { Reject = true };
ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher);
using var bus = EventBus.CreateBus(ShrinkBusKey.World(Guid.NewGuid().ToString()),
new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = 1 });
Assert.That(EventBus.PostDeferred(bus, new Evt()).Failure, Is.EqualTo(ShrinkPostFailure.SchedulerRejected));
dispatcher.Reject = false;
Assert.That(EventBus.PostDeferred(bus, new Evt()).Accepted, Is.True);
dispatcher.Pump();
EventBus.RemoveBus(bus.Key);
}
[Test]
public void NormalAndDeferredShareCapacityAndDisposeDropsPending()
{
var dispatcher = new Dispatcher { IsMainThread = false };
ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher);
using var bus = EventBus.CreateBus(ShrinkBusKey.World(Guid.NewGuid().ToString()),
new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = 1 });
Assert.That(bus.Post(new Evt()).Accepted, Is.True);
Assert.That(EventBus.PostDeferred(bus, new Evt()).Failure, Is.EqualTo(ShrinkPostFailure.QueueFull));
var dispatched = 0;
using var observation = EventBus.ObserveDiagnostics(trace =>
{
if (trace.BusKey == bus.Key) dispatched++;
});
EventBus.RemoveBus(bus.Key);
dispatcher.Pump();
Assert.That(dispatched, Is.Zero);
}
[Test]
public async Task RenameRecoveryNeverPromotesPartialTempAndKeepsBackups()
{
var root = Path.Combine(Path.GetTempPath(), "shrink-storage-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
Assert.Throws<ArgumentException>(() => new LocalStorageProvider(root, LocalStorageCommitStrategy.RenameOnly));
var storage = new LocalStorageProvider(root, LocalStorageCommitStrategy.RenameOnly,
(source, target) => File.Move(source, target, true));
File.WriteAllText(Path.Combine(root, "save.tmp"), "partial");
Assert.That(await storage.ExistsAsync("save"), Is.False);
await storage.WriteAsync("save", new byte[] { 1 });
await storage.WriteAsync("save", new byte[] { 2 });
await storage.WriteAsync("save", new byte[] { 3 });
Assert.That(await storage.ReadAsync("save.bak1"), Is.EqualTo(new byte[] { 2 }));
Assert.That(await storage.ReadAsync("save.bak2"), Is.EqualTo(new byte[] { 1 }));
File.Delete(Path.Combine(root, "save"));
File.WriteAllText(Path.Combine(root, "save.tmp"), "partial");
Assert.That(await storage.ExistsAsync("save"), Is.True);
Assert.That(await storage.ReadAsync("save"), Is.EqualTo(new byte[] { 2 }));
await storage.DeleteAsync("save");
Assert.That(await storage.ExistsAsync("save"), Is.False, "Deleted saves must not recover from backups.");
}
finally { Directory.Delete(root, true); }
}
[Test]
public async Task DeleteReportsLockedFileInsteadOfClaimingSuccess()
{
if (!OperatingSystem.IsWindows()) Assert.Ignore("Windows file sharing semantics.");
var root = Path.Combine(Path.GetTempPath(), "shrink-delete-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var storage = new LocalStorageProvider(root);
await storage.WriteAsync("save", new byte[] { 1 });
using var locked = new FileStream(Path.Combine(root, "save"), FileMode.Open, FileAccess.Read, FileShare.None);
Assert.ThrowsAsync<IOException>(async () => await storage.DeleteAsync("save"));
}
finally { Directory.Delete(root, true); }
}
private readonly struct Evt : IShrinkEvent { }
private sealed class Dispatcher : IShrinkMainThreadDispatcher, IShrinkDeferredMainThreadDispatcher
{
private readonly Queue<Action> _pending = new();
public bool IsMainThread { get; set; } = true;
public bool Reject { get; set; }
public int InlineCalls { get; private set; }
public bool TryPost(Action action) { InlineCalls++; action(); return true; }
public bool TryPostDeferred(Action action)
{
if (Reject) return false;
_pending.Enqueue(action);
return true;
}
public void Pump()
{
var count = _pending.Count;
while (count-- > 0) _pending.Dequeue()();
}
}
}