236 lines
6.6 KiB
C#
236 lines
6.6 KiB
C#
using FluentAssertions;
|
|
using Xunit;
|
|
|
|
namespace OECS.Tests;
|
|
|
|
public class InterruptTests
|
|
{
|
|
// ── Test types ──
|
|
|
|
private record struct TestInterrupt : IInterrupt
|
|
{
|
|
public string Message;
|
|
}
|
|
|
|
[MessagePack.MessagePackObject]
|
|
private struct TestHandler : IInterruptHandlerCommand<TestInterrupt>
|
|
{
|
|
[MessagePack.Key(0)]
|
|
public bool ShouldResolve;
|
|
|
|
public bool TryResolve(TestInterrupt interrupt)
|
|
{
|
|
return ShouldResolve;
|
|
}
|
|
}
|
|
|
|
private record struct AnotherInterrupt : IInterrupt
|
|
{
|
|
public int Value;
|
|
}
|
|
|
|
[MessagePack.MessagePackObject]
|
|
private struct AnotherHandler : IInterruptHandlerCommand<AnotherInterrupt>
|
|
{
|
|
[MessagePack.Key(0)]
|
|
public bool ShouldResolve;
|
|
|
|
public bool TryResolve(AnotherInterrupt interrupt)
|
|
{
|
|
return ShouldResolve;
|
|
}
|
|
}
|
|
|
|
// ── Issue and resolve ──
|
|
|
|
[Fact]
|
|
public void Interrupt_blocks_next_tick()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
var systemRan = false;
|
|
group.Add(new TestSystem(() => systemRan = true));
|
|
|
|
// Issue an interrupt.
|
|
world.Interrupt(new TestInterrupt { Message = "hello" });
|
|
|
|
// Run a tick — systems should be skipped.
|
|
group.RunLogical();
|
|
|
|
systemRan.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Interrupt_resolved_by_handler_unblocks_world()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
var systemRan = false;
|
|
group.Add(new TestSystem(() => systemRan = true));
|
|
|
|
// Issue an interrupt.
|
|
world.Interrupt(new TestInterrupt { Message = "hello" });
|
|
group.RunLogical();
|
|
systemRan.Should().BeFalse();
|
|
|
|
// Enqueue a handler that resolves it.
|
|
world.Commands.Enqueue(new TestHandler { ShouldResolve = true });
|
|
group.RunLogical();
|
|
systemRan.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Interrupt_handler_rejects_leaves_interrupt_pending()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
var systemRan = false;
|
|
group.Add(new TestSystem(() => systemRan = true));
|
|
|
|
world.Interrupt(new TestInterrupt { Message = "hello" });
|
|
group.RunLogical();
|
|
systemRan.Should().BeFalse();
|
|
|
|
// Handler rejects — interrupt stays pending.
|
|
world.Commands.Enqueue(new TestHandler { ShouldResolve = false });
|
|
group.RunLogical();
|
|
systemRan.Should().BeFalse();
|
|
|
|
// Second handler accepts.
|
|
world.Commands.Enqueue(new TestHandler { ShouldResolve = true });
|
|
group.RunLogical();
|
|
systemRan.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void HasInterrupt_returns_true_for_pending_interrupt()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
world.HasInterrupt<TestInterrupt>().Should().BeFalse();
|
|
|
|
world.Interrupt(new TestInterrupt { Message = "hello" });
|
|
world.HasInterrupt<TestInterrupt>().Should().BeTrue();
|
|
|
|
world.Commands.Enqueue(new TestHandler { ShouldResolve = true });
|
|
group.RunLogical();
|
|
|
|
world.HasInterrupt<TestInterrupt>().Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Duplicate_interrupt_type_throws()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
world.Interrupt(new TestInterrupt { Message = "first" });
|
|
|
|
var act = () => world.Interrupt(new TestInterrupt { Message = "second" });
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*TestInterrupt*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Multiple_interrupt_types_independent()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
world.Interrupt(new TestInterrupt { Message = "a" });
|
|
world.Interrupt(new AnotherInterrupt { Value = 42 });
|
|
|
|
// Resolve only one.
|
|
world.Commands.Enqueue(new TestHandler { ShouldResolve = true });
|
|
group.RunLogical();
|
|
|
|
// TestInterrupt resolved, AnotherInterrupt still pending.
|
|
world.HasInterrupt<TestInterrupt>().Should().BeFalse();
|
|
world.HasInterrupt<AnotherInterrupt>().Should().BeTrue();
|
|
|
|
// Resolve the other.
|
|
world.Commands.Enqueue(new AnotherHandler { ShouldResolve = true });
|
|
group.RunLogical();
|
|
|
|
world.HasInterrupt<AnotherInterrupt>().Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Handler_receives_correct_interrupt_data()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
world.Interrupt(new TestInterrupt { Message = "hello" });
|
|
|
|
// Enqueue handler and run — the default Execute calls TryResolve
|
|
// with the interrupt data.
|
|
world.Commands.Enqueue(new TestHandler { ShouldResolve = true });
|
|
group.RunLogical();
|
|
|
|
// Interrupt was resolved — the TryResolve received the correct data.
|
|
world.HasInterrupt<TestInterrupt>().Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Interrupt_without_system_group_is_noop()
|
|
{
|
|
var world = new World();
|
|
|
|
// No SystemGroup — interrupt is silently ignored.
|
|
world.Interrupt(new TestInterrupt { Message = "hello" });
|
|
world.HasInterrupt<TestInterrupt>().Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Commands_still_execute_when_blocked()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
var commandExecuted = false;
|
|
var handler = new TestHandler { ShouldResolve = true };
|
|
world.Interrupt(new TestInterrupt { Message = "hello" });
|
|
|
|
// Enqueue both a handler and a regular command.
|
|
world.Commands.Enqueue(new TestHandler { ShouldResolve = true });
|
|
world.Commands.Enqueue(new SetFlagCommand(() => commandExecuted = true));
|
|
|
|
group.RunLogical();
|
|
|
|
// Both should have executed.
|
|
world.HasInterrupt<TestInterrupt>().Should().BeFalse();
|
|
commandExecuted.Should().BeTrue();
|
|
}
|
|
|
|
// ── Helpers ──
|
|
|
|
private class TestSystem : ISystem
|
|
{
|
|
private readonly Action _action;
|
|
public TestSystem(Action action) => _action = action;
|
|
public void RunImpl(World world) => _action();
|
|
}
|
|
|
|
[MessagePack.MessagePackObject]
|
|
private struct SetFlagCommand : ICommand
|
|
{
|
|
private Action? _action;
|
|
|
|
[MessagePack.IgnoreMember]
|
|
public Action Action
|
|
{
|
|
get => _action!;
|
|
set => _action = value;
|
|
}
|
|
|
|
public SetFlagCommand(Action action) => _action = action;
|
|
|
|
public void Execute(World world) => _action?.Invoke();
|
|
}
|
|
}
|