From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:03:44 +0000 (+1300) Subject: Add generic event listener for integration tests (#40367) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=dac2c5212ae1c5f230f04dc31e7ade293dc8dcae;p=space-station-14.git Add generic event listener for integration tests (#40367) * Add generic event listener for integration tests * cleanup * assert that the entity has the component * comments & new overload --- diff --git a/Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs b/Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs new file mode 100644 index 0000000000..817558b426 --- /dev/null +++ b/Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.Helpers; + +/// +/// Component that is used by to store any information about received events. +/// +[RegisterComponent] +public sealed partial class TestListenerComponent : Component +{ + public Dictionary> Events = new(); +} diff --git a/Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs b/Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs new file mode 100644 index 0000000000..2481cef03f --- /dev/null +++ b/Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs @@ -0,0 +1,45 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Helpers; + +/// +/// Generic system that listens for and records any received events of a given type. +/// +public abstract class TestListenerSystem : EntitySystem where TEvent : notnull +{ + public override void Initialize() + { + // TODO + // supporting broadcast events requires cleanup on test finish, which will probably require changes to the + // test pair/pool manager and would conflict with #36797 + SubscribeLocalEvent(OnDirectedEvent); + } + + protected virtual void OnDirectedEvent(Entity ent, ref TEvent args) + { + ent.Comp.Events.GetOrNew(args.GetType()).Add(args); + } + + public int Count(EntityUid uid, Func? predicate = null) + { + return GetEvents(uid, predicate).Count(); + } + + public void Clear(EntityUid uid) + { + CompOrNull(uid)?.Events.Remove(typeof(TEvent)); + } + + public IEnumerable GetEvents(EntityUid uid, Func? predicate = null) + { + var events = CompOrNull(uid)?.Events.GetValueOrDefault(typeof(TEvent)); + if (events == null) + return []; + + return events.Cast().Where(e => predicate?.Invoke(e) ?? true); + } +} diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index fa16730dd5..d04ed4cb3c 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Numerics; using System.Reflection; using Content.Client.Construction; +using Content.IntegrationTests.Tests.Helpers; using Content.Server.Atmos.EntitySystems; using Content.Server.Construction.Components; using Content.Server.Gravity; @@ -22,6 +23,8 @@ using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Maths; +using Robust.Shared.Reflection; +using Robust.UnitTesting; using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent; namespace Content.IntegrationTests.Tests.Interaction; @@ -29,6 +32,8 @@ namespace Content.IntegrationTests.Tests.Interaction; // This partial class defines various methods that are useful for performing & validating interactions public abstract partial class InteractionTest { + private Dictionary _listenerCache = new(); + /// /// Begin constructing an entity. /// @@ -758,6 +763,139 @@ public abstract partial class InteractionTest #endregion + #region EventListener + + /// + /// Asserts that running the given action causes an event to be fired directed at the specified entity (defaults to ). + /// + /// + /// This currently only checks server-side events. + /// + /// The entity at which the events are supposed to be directed + /// How many new events are expected + /// Whether to clear all previously recorded events before invoking the delegate + protected async Task AssertFiresEvent(Func act, EntityUid? uid = null, int count = 1, bool clear = true) + where TEvent : notnull + { + var sys = GetListenerSystem(); + + uid ??= STarget; + if (uid == null) + { + Assert.Fail("No target specified"); + return; + } + + if (clear) + sys.Clear(uid.Value); + else + count += sys.Count(uid.Value); + + await Server.WaitPost(() => SEntMan.EnsureComponent(uid.Value)); + await act(); + AssertEvent(uid, count: count); + } + + /// + /// This is a variant of that passes the delegate to . + /// + /// + /// This currently only checks for server-side events. + /// + /// The entity at which the events are supposed to be directed + /// How many new events are expected + /// Whether to clear all previously recorded events before invoking the delegate + protected async Task AssertPostFiresEvent(Action act, EntityUid? uid = null, int count = 1, bool clear = true) + where TEvent : notnull + { + await AssertFiresEvent(async () => await Server.WaitPost(act), uid, count, clear); + } + + /// + /// This is a variant of that passes the delegate to . + /// + /// + /// This currently only checks for server-side events. + /// + /// The entity at which the events are supposed to be directed + /// How many new events are expected + /// Whether to clear all previously recorded events before invoking the delegate + protected async Task AssertAssertionFiresEvent(Action act, + EntityUid? uid = null, + int count = 1, + bool clear = true) + where TEvent : notnull + { + await AssertFiresEvent(async () => await Server.WaitAssertion(act), uid, count, clear); + } + + /// + /// Asserts that the specified event has been fired some number of times at the given entity (defaults to ). + /// For this to work, this requires that the entity has been given a + /// + /// + /// This currently only checks server-side events. + /// + /// The entity at which the events were directed + /// How many new events are expected + /// A predicate that can be used to filter the recorded events + protected void AssertEvent(EntityUid? uid = null, int count = 1, Func? predicate = null) + where TEvent : notnull + { + Assert.That(GetEvents(uid, predicate).Count, Is.EqualTo(count)); + } + + /// + /// Gets all the events of the specified type that have been fired at the given entity (defaults to ). + /// For this to work, this requires that the entity has been given a + /// + /// + /// This currently only gets for server-side events. + /// + /// The entity at which the events were directed + /// A predicate that can be used to filter the returned events + protected IEnumerable GetEvents(EntityUid? uid = null, Func? predicate = null) + where TEvent : notnull + { + uid ??= STarget; + if (uid == null) + { + Assert.Fail("No target specified"); + return []; + } + + Assert.That(SEntMan.HasComponent(uid), $"Entity must have {nameof(TestListenerComponent)}"); + return GetListenerSystem().GetEvents(uid.Value, predicate); + } + + protected TestListenerSystem GetListenerSystem() + where TEvent : notnull + { + if (_listenerCache.TryGetValue(typeof(TEvent), out var listener)) + return (TestListenerSystem) listener; + + var type = Server.Resolve().GetAllChildren>().Single(); + if (!SEntMan.EntitySysManager.TryGetEntitySystem(type, out var systemObj)) + { + // There has to be a listener system that is manually defined. Event subscriptions are locked once + // finalized, so we can't really easily create new subscriptions on the fly. + // TODO find a better solution + throw new InvalidOperationException($"Event {typeof(TEvent).Name} has no associated listener system!"); + } + + var system = (TestListenerSystem)systemObj; + _listenerCache[typeof(TEvent)] = system; + return system; + } + + /// + /// Clears all recorded events of the given type. + /// + protected void ClearEvents(EntityUid uid) where TEvent : notnull + => GetListenerSystem().Clear(uid); + + #endregion + #region Entity lookups /// diff --git a/Content.IntegrationTests/Tests/Movement/SlippingTest.cs b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs index 7ee895d7c2..92e4d2471e 100644 --- a/Content.IntegrationTests/Tests/Movement/SlippingTest.cs +++ b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs @@ -1,10 +1,8 @@ #nullable enable -using System.Collections.Generic; -using Content.IntegrationTests.Tests.Interaction; +using Content.IntegrationTests.Tests.Helpers; using Content.Shared.Movement.Components; using Content.Shared.Slippery; using Content.Shared.Stunnable; -using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.Maths; @@ -12,44 +10,32 @@ namespace Content.IntegrationTests.Tests.Movement; public sealed class SlippingTest : MovementTest { - public sealed class SlipTestSystem : EntitySystem - { - public HashSet Slipped = new(); - public override void Initialize() - { - SubscribeLocalEvent(OnSlip); - } - - private void OnSlip(EntityUid uid, SlipperyComponent component, ref SlipEvent args) - { - Slipped.Add(args.Slipped); - } - } + public sealed class SlipTestSystem : TestListenerSystem; [Test] public async Task BananaSlipTest() { - var sys = SEntMan.System(); await SpawnTarget("TrashBananaPeel"); var modifier = Comp(Player).SprintSpeedModifier; Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed."); - // Player is to the left of the banana peel and has not slipped. + // Player is to the left of the banana peel. Assert.That(Delta(), Is.GreaterThan(0.5f)); - Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); // Walking over the banana slowly does not trigger a slip. await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down); - await Move(DirectionFlag.East, 1f); + await AssertFiresEvent(async () => await Move(DirectionFlag.East, 1f), count: 0); + Assert.That(Delta(), Is.LessThan(0.5f)); - Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); AssertComp(false, Player); // Moving at normal speeds does trigger a slip. await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Up); - await Move(DirectionFlag.West, 1f); - Assert.That(sys.Slipped, Does.Contain(SEntMan.GetEntity(Player))); + await AssertFiresEvent(async () => await Move(DirectionFlag.West, 1f)); + + // And the person that slipped was the player + AssertEvent(predicate: @event => @event.Slipped == SPlayer); AssertComp(true, Player); } }