feat: integrate composable Godot runtime and EventBus debugger
This commit is contained in:
55 files changed
+1481
-114
No files matched your search
@@ -0,0 +1,168 @@
|
||||
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()();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user