169 lines
7.1 KiB
C#
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()();
|
|
}
|
|
}
|
|
}
|