using System.Collections.Generic; using Moq; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager; using Robust.UnitTesting.Shared.Reflection; namespace Robust.UnitTesting.Shared.GameObjects { public sealed partial class EntityEventBusTests { [Test] public void SubscribeCompEvent() { var compFactory = new ComponentFactory(new DynamicTypeFactory(), new ReflectionManagerTest(), new SerializationManager(), new LogManager()); // Arrange var entUid = new EntityUid(7); var compInstance = new MetaDataComponent(); var entManMock = new Mock(); var reflectMock = new Mock(); compFactory.RegisterClass(); entManMock.Setup(m => m.ComponentFactory).Returns(compFactory); IComponent? outIComponent = compInstance; entManMock.Setup(m => m.TryGetComponent(entUid, CompIdx.Index(), out outIComponent)) .Returns(true); entManMock.Setup(m => m.GetComponent(entUid, CompIdx.Index())) .Returns(compInstance); entManMock.Setup(m => m.GetComponentInternal(entUid, CompIdx.Index())) .Returns(compInstance); var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); // Subscribe int calledCount = 0; bus.SubscribeLocalEvent(HandleTestEvent); bus.LockSubscriptions(); // add a component to the system bus.OnEntityAdded(entUid); var reg = compFactory.GetRegistration(CompIdx.Index()); bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); // Raise var evntArgs = new TestEvent(5); bus.RaiseLocalEvent(entUid, evntArgs, true); // Assert Assert.That(calledCount, Is.EqualTo(1)); void HandleTestEvent(EntityUid uid, MetaDataComponent component, TestEvent args) { calledCount++; Assert.That(uid, Is.EqualTo(entUid)); Assert.That(component, Is.EqualTo(compInstance)); Assert.That(args.TestNumber, Is.EqualTo(5)); } } [Test] public void UnsubscribeCompEvent() { // Arrange var entUid = new EntityUid(7); var compInstance = new MetaDataComponent(); var entManMock = new Mock(); var compRegistration = new ComponentRegistration( "MetaData", typeof(MetaDataComponent), CompIdx.Index()); var compFacMock = new Mock(); var reflectMock = new Mock(); compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(compRegistration); compFacMock.Setup(m => m.GetAllRegistrations()).Returns(new[] { compRegistration }); compFacMock.Setup(m => m.GetIndex(typeof(MetaDataComponent))).Returns(CompIdx.Index()); entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); IComponent? outIComponent = compInstance; entManMock.Setup(m => m.TryGetComponent(entUid, typeof(MetaDataComponent), out outIComponent)) .Returns(true); entManMock.Setup(m => m.GetComponent(entUid, typeof(MetaDataComponent))) .Returns(compInstance); var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); // Subscribe int calledCount = 0; bus.SubscribeLocalEvent(HandleTestEvent); bus.UnsubscribeLocalEvent(); bus.LockSubscriptions(); // add a component to the system bus.OnEntityAdded(entUid); var reg = compFacMock.Object.GetRegistration(CompIdx.Index()); bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); // Raise var evntArgs = new TestEvent(5); bus.RaiseLocalEvent(entUid, evntArgs, true); // Assert Assert.That(calledCount, Is.EqualTo(0)); void HandleTestEvent(EntityUid uid, MetaDataComponent component, TestEvent args) { calledCount++; } } [Test] public void SubscribeCompLifeEvent() { // Arrange var entUid = new EntityUid(7); var compInstance = new MetaDataComponent(); var entManMock = new Mock(); #pragma warning disable CS0618 // Type or member is obsolete compInstance.Owner = entUid; #pragma warning restore CS0618 // Type or member is obsolete var compRegistration = new ComponentRegistration( "MetaData", typeof(MetaDataComponent), CompIdx.Index()); var compFacMock = new Mock(); var reflectMock = new Mock(); compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(compRegistration); compFacMock.Setup(m => m.GetAllRegistrations()).Returns(new[] { compRegistration }); compFacMock.Setup(m => m.GetIndex(typeof(MetaDataComponent))).Returns(CompIdx.Index()); entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); IComponent? outIComponent = compInstance; entManMock.Setup(m => m.TryGetComponent(entUid, typeof(MetaDataComponent), out outIComponent)) .Returns(true); entManMock.Setup(m => m.GetComponent(entUid, typeof(MetaDataComponent))) .Returns(compInstance); var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); // Subscribe int calledCount = 0; bus.SubscribeLocalEvent(HandleTestEvent); bus.LockSubscriptions(); // add a component to the system entManMock.Raise(m => m.EntityAdded += null, entUid); var reg = compFacMock.Object.GetRegistration(); entManMock.Raise(m => m.ComponentAdded += null, new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); // Raise ((IEventBus)bus).RaiseComponentEvent(entUid, compInstance, new ComponentInit()); // Assert Assert.That(calledCount, Is.EqualTo(1)); void HandleTestEvent(EntityUid uid, MetaDataComponent component, ComponentInit args) { calledCount++; Assert.That(uid, Is.EqualTo(entUid)); Assert.That(component, Is.EqualTo(compInstance)); } } [Test] public void CompEventOrdered() { // Arrange var entUid = new EntityUid(7); var entManMock = new Mock(); var compFacMock = new Mock(); var reflectMock = new Mock(); List allRefTypes = new(); void Setup(out T instance) where T : IComponent, new() { IComponent? inst = instance = new T(); var reg = new ComponentRegistration( typeof(T).Name, typeof(T), CompIdx.Index()); compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(reg); compFacMock.Setup(m => m.GetIndex(typeof(T))).Returns(CompIdx.Index()); entManMock.Setup(m => m.TryGetComponent(entUid, CompIdx.Index(), out inst)).Returns(true); entManMock.Setup(m => m.GetComponent(entUid, CompIdx.Index())).Returns(inst); entManMock.Setup(m => m.GetComponentInternal(entUid, CompIdx.Index())).Returns(inst); allRefTypes.Add(reg); } Setup(out var instA); Setup(out var instB); Setup(out var instC); compFacMock.Setup(m => m.GetAllRegistrations()).Returns(allRefTypes.ToArray()); entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); // Subscribe var a = false; var b = false; var c = false; void HandlerA(EntityUid uid, Component comp, TestEvent ev) { Assert.That(b, Is.False, "A should run before B"); Assert.That(c, Is.False, "A should run before C"); a = true; } void HandlerB(EntityUid uid, Component comp, TestEvent ev) { Assert.That(c, Is.True, "B should run after C"); b = true; } void HandlerC(EntityUid uid, Component comp, TestEvent ev) => c = true; bus.SubscribeLocalEvent(HandlerA, typeof(OrderAComponent), before: new []{typeof(OrderBComponent), typeof(OrderCComponent)}); bus.SubscribeLocalEvent(HandlerB, typeof(OrderBComponent), after: new []{typeof(OrderCComponent)}); bus.SubscribeLocalEvent(HandlerC, typeof(OrderCComponent)); bus.LockSubscriptions(); // add a component to the system bus.OnEntityAdded(entUid); var regA = compFacMock.Object.GetRegistration(CompIdx.Index()); var regB = compFacMock.Object.GetRegistration(CompIdx.Index()); var regC = compFacMock.Object.GetRegistration(CompIdx.Index()); bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instC, entUid), regC)); // Raise var evntArgs = new TestEvent(5); bus.RaiseLocalEvent(entUid, evntArgs, true); // Assert Assert.That(a, Is.True, "A did not fire"); Assert.That(b, Is.True, "B did not fire"); Assert.That(c, Is.True, "C did not fire"); } [Test] public void CompEventLoop() { var entUid = new EntityUid(7); var entManMock = new Mock(); var compFacMock = new Mock(); var reflectMock = new Mock(); List allRefTypes = new(); void Setup(out T instance) where T : IComponent, new() { IComponent? inst = instance = new T(); var reg = new ComponentRegistration( typeof(T).Name, typeof(T), CompIdx.Index()); compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(reg); compFacMock.Setup(m => m.GetIndex(typeof(T))).Returns(CompIdx.Index()); entManMock.Setup(m => m.TryGetComponent(entUid, CompIdx.Index(), out inst)).Returns(true); entManMock.Setup(m => m.GetComponent(entUid, CompIdx.Index())).Returns(inst); entManMock.Setup(m => m.GetComponentInternal(entUid, CompIdx.Index())).Returns(inst); allRefTypes.Add(reg); } Setup(out var instA); Setup(out var instB); compFacMock.Setup(m => m.GetAllRegistrations()).Returns(allRefTypes.ToArray()); entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); var regA = compFacMock.Object.GetRegistration(CompIdx.Index()); var regB = compFacMock.Object.GetRegistration(CompIdx.Index()); var handlerACount = 0; void HandlerA(EntityUid uid, Component comp, TestEvent ev) { Assert.That(handlerACount, Is.EqualTo(0)); handlerACount++; // add and then remove component B bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instB, entUid), false, default!, CompIdx.Index())); bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); } var handlerBCount = 0; void HandlerB(EntityUid uid, Component comp, TestEvent ev) { Assert.That(handlerBCount, Is.EqualTo(0)); handlerBCount++; // add and then remove component A bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instA, entUid), false, default!, CompIdx.Index())); bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); } bus.SubscribeLocalEvent(HandlerA, typeof(OrderAComponent)); bus.SubscribeLocalEvent(HandlerB, typeof(OrderBComponent)); bus.LockSubscriptions(); // add a component to the system bus.OnEntityAdded(entUid); bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); // Event subscriptions currently use a linked list. // Currently expect event subscriptions to be raised in order: handlerB -> handlerA // If a component gets removed and added again, it gets moved back to the front of the linked list. // I.e., adding and then removing compA changes the linked list order: handlerA -> handlerB // // This could in principle cause the event raising code to enter an infinite loop. // Adding and removing a comp in an event handler may seem silly but: // - it doesn't have to be the same component if you had a chain of three or more components // - some event handlers raise other events and can lead to convoluted chains of interactions that might inadvertently trigger something like this. // Raise bus.RaiseLocalEvent(entUid, new TestEvent(0)); // Assert Assert.That(handlerACount, Is.LessThanOrEqualTo(1)); Assert.That(handlerBCount, Is.LessThanOrEqualTo(1)); Assert.That(handlerACount+handlerBCount, Is.GreaterThan(0)); } private sealed partial class DummyComponent : Component { } private sealed partial class OrderAComponent : Component { } private sealed partial class OrderBComponent : Component { } private sealed partial class OrderCComponent : Component { } private sealed class TestEvent : EntityEventArgs { public int TestNumber { get; } public TestEvent(int testNumber) { TestNumber = testNumber; } } } }