oecs-sharp/OECS.Tests/InterruptTests.cs

236 lines
6.6 KiB
C#
Raw Permalink Normal View History

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();
}
}