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
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component that is used by <see cref="TestListenerSystem{TEvent}"/> to store any information about received events.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed partial class TestListenerComponent : Component
|
||||||
|
{
|
||||||
|
public Dictionary<Type, List<object>> Events = new();
|
||||||
|
}
|
||||||
45
Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs
Normal file
45
Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generic system that listens for and records any received events of a given type.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class TestListenerSystem<TEvent> : 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<TestListenerComponent, TEvent>(OnDirectedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnDirectedEvent(Entity<TestListenerComponent> ent, ref TEvent args)
|
||||||
|
{
|
||||||
|
ent.Comp.Events.GetOrNew(args.GetType()).Add(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count(EntityUid uid, Func<TEvent, bool>? predicate = null)
|
||||||
|
{
|
||||||
|
return GetEvents(uid, predicate).Count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear(EntityUid uid)
|
||||||
|
{
|
||||||
|
CompOrNull<TestListenerComponent>(uid)?.Events.Remove(typeof(TEvent));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<TEvent> GetEvents(EntityUid uid, Func<TEvent, bool>? predicate = null)
|
||||||
|
{
|
||||||
|
var events = CompOrNull<TestListenerComponent>(uid)?.Events.GetValueOrDefault(typeof(TEvent));
|
||||||
|
if (events == null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return events.Cast<TEvent>().Where(e => predicate?.Invoke(e) ?? true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Content.Client.Construction;
|
using Content.Client.Construction;
|
||||||
|
using Content.IntegrationTests.Tests.Helpers;
|
||||||
using Content.Server.Atmos.EntitySystems;
|
using Content.Server.Atmos.EntitySystems;
|
||||||
using Content.Server.Construction.Components;
|
using Content.Server.Construction.Components;
|
||||||
using Content.Server.Gravity;
|
using Content.Server.Gravity;
|
||||||
@@ -22,6 +23,8 @@ using Robust.Shared.Input;
|
|||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Reflection;
|
||||||
|
using Robust.UnitTesting;
|
||||||
using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent;
|
using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent;
|
||||||
|
|
||||||
namespace Content.IntegrationTests.Tests.Interaction;
|
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
|
// This partial class defines various methods that are useful for performing & validating interactions
|
||||||
public abstract partial class InteractionTest
|
public abstract partial class InteractionTest
|
||||||
{
|
{
|
||||||
|
private Dictionary<Type, EntitySystem> _listenerCache = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Begin constructing an entity.
|
/// Begin constructing an entity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -758,6 +763,139 @@ public abstract partial class InteractionTest
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region EventListener
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that running the given action causes an event to be fired directed at the specified entity (defaults to <see cref="Target"/>).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This currently only checks server-side events.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="uid">The entity at which the events are supposed to be directed</param>
|
||||||
|
/// <param name="count">How many new events are expected</param>
|
||||||
|
/// <param name="clear">Whether to clear all previously recorded events before invoking the delegate</param>
|
||||||
|
protected async Task AssertFiresEvent<TEvent>(Func<Task> act, EntityUid? uid = null, int count = 1, bool clear = true)
|
||||||
|
where TEvent : notnull
|
||||||
|
{
|
||||||
|
var sys = GetListenerSystem<TEvent>();
|
||||||
|
|
||||||
|
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<TestListenerComponent>(uid.Value));
|
||||||
|
await act();
|
||||||
|
AssertEvent<TEvent>(uid, count: count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a variant of <see cref="AssertFiresEvent{TEvent}"/> that passes the delegate to <see cref="RobustIntegrationTest.ServerIntegrationInstance.WaitPost"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This currently only checks for server-side events.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="uid">The entity at which the events are supposed to be directed</param>
|
||||||
|
/// <param name="count">How many new events are expected</param>
|
||||||
|
/// <param name="clear">Whether to clear all previously recorded events before invoking the delegate</param>
|
||||||
|
protected async Task AssertPostFiresEvent<TEvent>(Action act, EntityUid? uid = null, int count = 1, bool clear = true)
|
||||||
|
where TEvent : notnull
|
||||||
|
{
|
||||||
|
await AssertFiresEvent<TEvent>(async () => await Server.WaitPost(act), uid, count, clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a variant of <see cref="AssertFiresEvent{TEvent}"/> that passes the delegate to <see cref="RobustIntegrationTest.ServerIntegrationInstance.WaitAssertion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This currently only checks for server-side events.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="uid">The entity at which the events are supposed to be directed</param>
|
||||||
|
/// <param name="count">How many new events are expected</param>
|
||||||
|
/// <param name="clear">Whether to clear all previously recorded events before invoking the delegate</param>
|
||||||
|
protected async Task AssertAssertionFiresEvent<TEvent>(Action act,
|
||||||
|
EntityUid? uid = null,
|
||||||
|
int count = 1,
|
||||||
|
bool clear = true)
|
||||||
|
where TEvent : notnull
|
||||||
|
{
|
||||||
|
await AssertFiresEvent<TEvent>(async () => await Server.WaitAssertion(act), uid, count, clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that the specified event has been fired some number of times at the given entity (defaults to <see cref="Target"/>).
|
||||||
|
/// For this to work, this requires that the entity has been given a <see cref="TestListenerComponent"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This currently only checks server-side events.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="uid">The entity at which the events were directed</param>
|
||||||
|
/// <param name="count">How many new events are expected</param>
|
||||||
|
/// <param name="predicate">A predicate that can be used to filter the recorded events</param>
|
||||||
|
protected void AssertEvent<TEvent>(EntityUid? uid = null, int count = 1, Func<TEvent,bool>? predicate = null)
|
||||||
|
where TEvent : notnull
|
||||||
|
{
|
||||||
|
Assert.That(GetEvents(uid, predicate).Count, Is.EqualTo(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all the events of the specified type that have been fired at the given entity (defaults to <see cref="Target"/>).
|
||||||
|
/// For this to work, this requires that the entity has been given a <see cref="TestListenerComponent"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This currently only gets for server-side events.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="uid">The entity at which the events were directed</param>
|
||||||
|
/// <param name="predicate">A predicate that can be used to filter the returned events</param>
|
||||||
|
protected IEnumerable<TEvent> GetEvents<TEvent>(EntityUid? uid = null, Func<TEvent, bool>? predicate = null)
|
||||||
|
where TEvent : notnull
|
||||||
|
{
|
||||||
|
uid ??= STarget;
|
||||||
|
if (uid == null)
|
||||||
|
{
|
||||||
|
Assert.Fail("No target specified");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.That(SEntMan.HasComponent<TestListenerComponent>(uid), $"Entity must have {nameof(TestListenerComponent)}");
|
||||||
|
return GetListenerSystem<TEvent>().GetEvents(uid.Value, predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TestListenerSystem<TEvent> GetListenerSystem<TEvent>()
|
||||||
|
where TEvent : notnull
|
||||||
|
{
|
||||||
|
if (_listenerCache.TryGetValue(typeof(TEvent), out var listener))
|
||||||
|
return (TestListenerSystem<TEvent>) listener;
|
||||||
|
|
||||||
|
var type = Server.Resolve<IReflectionManager>().GetAllChildren<TestListenerSystem<TEvent>>().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<TEvent>)systemObj;
|
||||||
|
_listenerCache[typeof(TEvent)] = system;
|
||||||
|
return system;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all recorded events of the given type.
|
||||||
|
/// </summary>
|
||||||
|
protected void ClearEvents<TEvent>(EntityUid uid) where TEvent : notnull
|
||||||
|
=> GetListenerSystem<TEvent>().Clear(uid);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Entity lookups
|
#region Entity lookups
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Collections.Generic;
|
using Content.IntegrationTests.Tests.Helpers;
|
||||||
using Content.IntegrationTests.Tests.Interaction;
|
|
||||||
using Content.Shared.Movement.Components;
|
using Content.Shared.Movement.Components;
|
||||||
using Content.Shared.Slippery;
|
using Content.Shared.Slippery;
|
||||||
using Content.Shared.Stunnable;
|
using Content.Shared.Stunnable;
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.Input;
|
using Robust.Shared.Input;
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
|
|
||||||
@@ -12,44 +10,32 @@ namespace Content.IntegrationTests.Tests.Movement;
|
|||||||
|
|
||||||
public sealed class SlippingTest : MovementTest
|
public sealed class SlippingTest : MovementTest
|
||||||
{
|
{
|
||||||
public sealed class SlipTestSystem : EntitySystem
|
public sealed class SlipTestSystem : TestListenerSystem<SlipEvent>;
|
||||||
{
|
|
||||||
public HashSet<EntityUid> Slipped = new();
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
SubscribeLocalEvent<SlipperyComponent, SlipEvent>(OnSlip);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSlip(EntityUid uid, SlipperyComponent component, ref SlipEvent args)
|
|
||||||
{
|
|
||||||
Slipped.Add(args.Slipped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task BananaSlipTest()
|
public async Task BananaSlipTest()
|
||||||
{
|
{
|
||||||
var sys = SEntMan.System<SlipTestSystem>();
|
|
||||||
await SpawnTarget("TrashBananaPeel");
|
await SpawnTarget("TrashBananaPeel");
|
||||||
|
|
||||||
var modifier = Comp<MovementSpeedModifierComponent>(Player).SprintSpeedModifier;
|
var modifier = Comp<MovementSpeedModifierComponent>(Player).SprintSpeedModifier;
|
||||||
Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed.");
|
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(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.
|
// Walking over the banana slowly does not trigger a slip.
|
||||||
await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down);
|
await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down);
|
||||||
await Move(DirectionFlag.East, 1f);
|
await AssertFiresEvent<SlipEvent>(async () => await Move(DirectionFlag.East, 1f), count: 0);
|
||||||
|
|
||||||
Assert.That(Delta(), Is.LessThan(0.5f));
|
Assert.That(Delta(), Is.LessThan(0.5f));
|
||||||
Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player)));
|
|
||||||
AssertComp<KnockedDownComponent>(false, Player);
|
AssertComp<KnockedDownComponent>(false, Player);
|
||||||
|
|
||||||
// Moving at normal speeds does trigger a slip.
|
// Moving at normal speeds does trigger a slip.
|
||||||
await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Up);
|
await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Up);
|
||||||
await Move(DirectionFlag.West, 1f);
|
await AssertFiresEvent<SlipEvent>(async () => await Move(DirectionFlag.West, 1f));
|
||||||
Assert.That(sys.Slipped, Does.Contain(SEntMan.GetEntity(Player)));
|
|
||||||
|
// And the person that slipped was the player
|
||||||
|
AssertEvent<SlipEvent>(predicate: @event => @event.Slipped == SPlayer);
|
||||||
AssertComp<KnockedDownComponent>(true, Player);
|
AssertComp<KnockedDownComponent>(true, Player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user