using System; using System.Collections.Generic; using Amphora; using UnitTestSharp; namespace Ariadne.UnitTests { /// /// A slot that logs a tag each time it is woken. /// public class RecordingSlot : WakeSlot { private readonly List woken; private readonly int tag; public RecordingSlot(List woken, int order, int tag) { this.woken = woken; this.tag = tag; Order = order; } public RecordingSlot(List woken, int order) : this(woken, order, tag: order) { } public override void Wake() { woken.Add(tag); } } public class TriggerTests : TestFixture { public static int[] Counting(int count) { var returnMe = new int[count]; for (int i = 0; i < count; ++i) { returnMe[i] = i; } return returnMe; } /// /// Returns a wait on for an arc that logs when woken. /// public static TriggerWait WaitOf(List woken, int order, int tag, bool requiresAll, params Trigger[] triggers) { var returnMe = new TriggerWait(capacity: Math.Max(1, triggers.Length)); returnMe.Begin(new Frame { Wake = new RecordingSlot(woken, order, tag) }, requiresAll); foreach (Trigger trigger in triggers) { returnMe.Add(trigger); } return returnMe; } public static TriggerWait WaitOf(List woken, int order, params Trigger[] triggers) { return WaitOf(woken, order, tag: order, requiresAll: false, triggers: triggers); } public class SubscribeTests : TestFixture { private List woken; private Trigger trigger; public override void TestSetup() { woken = new List(); trigger = new Trigger(); } public void NullSubscription_Throws() { CheckThrow(typeof(ArgumentNullException)); trigger.Subscribe(null, wait: 1); } public void Subscription_IsCounted() { WaitOf(woken, order: 0, trigger); CheckEqual(1, trigger.SubscriptionCount); } public void ManyLiveSubscriptions_AreAllKept() { for (int order = 0; order < 100; ++order) { WaitOf(woken, order, trigger); } CheckEqual(100, trigger.SubscriptionCount); } public void SubscriptionsOfWaitsThatBeganAgain_AreSweptWhenTheListFills() { TriggerWait wait = WaitOf(woken, order: 0, trigger); for (int i = 0; i < 1000; ++i) { wait.Begin(new Frame { Wake = wait.Slot }, requiresAll: false); wait.Add(trigger); } CheckTrue(trigger.SubscriptionCount <= Trigger.InitialCapacity); } public void SubscriptionsOfWaitsThatAreOver_AreSweptWhenTheListFills() { for (int i = 0; i < 1000; ++i) { WaitOf(woken, order: 0, trigger).Mark(0); } CheckTrue(trigger.SubscriptionCount <= Trigger.InitialCapacity); } public void SubscriptionsOfDroppedSlots_AreSweptWhenTheListFills() { for (int i = 0; i < 1000; ++i) { WaitOf(woken, order: 0, trigger).Slot.Dropped = true; } CheckTrue(trigger.SubscriptionCount <= Trigger.InitialCapacity); } public void SubscriptionsOfUnattachedWaits_AreKeptWhenTheListFills() { for (int i = 0; i < 20; ++i) { WaitOf(woken, order: 0, trigger).Slot = null; } CheckEqual(20, trigger.SubscriptionCount); } public void SweepWhenTheListFills_KeepsTheLiveSubscriptions() { WaitOf(woken, order: 0, trigger); TriggerWait restless = WaitOf(woken, order: 1, trigger); for (int i = 0; i < 1000; ++i) { restless.Begin(new Frame { Wake = restless.Slot }, requiresAll: false); restless.Add(trigger); } trigger.Raise(); CheckEqual(new[] { 0, 1 }, woken.ToArray()); } } public class SubscriptionAtTests : TestFixture { private List woken; private Trigger trigger; public override void TestSetup() { woken = new List(); trigger = new Trigger(); } public void ReturnsTheSubscriptionsInTheOrderTheyWereMade() { TriggerWait first = WaitOf(woken, order: 0, trigger); TriggerWait second = WaitOf(woken, order: 1, trigger); CheckTrue(ReferenceEquals(first.Subscriptions[0], trigger.SubscriptionAt(0))); CheckTrue(ReferenceEquals(second.Subscriptions[0], trigger.SubscriptionAt(1))); } public void NegativeIndex_Throws() { WaitOf(woken, order: 0, trigger); CheckThrow(typeof(ArgumentOutOfRangeException)); trigger.SubscriptionAt(-1); } public void IndexPastTheCount_Throws() { WaitOf(woken, order: 0, trigger); CheckThrow(typeof(ArgumentOutOfRangeException)); trigger.SubscriptionAt(1); } } public class WaitAtTests : TestFixture { private List woken; private Trigger trigger; public override void TestSetup() { woken = new List(); trigger = new Trigger(); } public void ReturnsTheNumberOfTheWaitTheSubscriptionWasMadeFor() { TriggerWait wait = WaitOf(woken, order: 0, trigger); wait.Begin(new Frame { Wake = wait.Slot }, requiresAll: false); wait.Add(trigger); CheckEqual(1, trigger.WaitAt(0)); CheckEqual(2, trigger.WaitAt(1)); } public void IndexPastTheCount_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); trigger.WaitAt(0); } } public class IsLiveAtTests : TestFixture { private List woken; private Trigger trigger; public override void TestSetup() { woken = new List(); trigger = new Trigger(); } public void SubscriptionOfAWaitInProgress_IsLive() { WaitOf(woken, order: 0, trigger); CheckTrue(trigger.IsLiveAt(0)); } public void SubscriptionOfAWaitThatBeganAgain_IsNotLive() { TriggerWait wait = WaitOf(woken, order: 0, trigger); wait.Begin(new Frame { Wake = wait.Slot }, requiresAll: false); CheckFalse(trigger.IsLiveAt(0)); } public void SubscriptionOfAWaitThatIsOver_IsNotLive() { WaitOf(woken, order: 0, trigger).Mark(0); CheckFalse(trigger.IsLiveAt(0)); } public void SubscriptionOfADroppedSlot_IsNotLive() { WaitOf(woken, order: 0, trigger).Slot.Dropped = true; CheckFalse(trigger.IsLiveAt(0)); } public void SubscriptionOfAnUnattachedWait_IsLive() { WaitOf(woken, order: 0, trigger).Slot = null; CheckTrue(trigger.IsLiveAt(0)); } public void IndexPastTheCount_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); trigger.IsLiveAt(0); } } public class RestoreTests : TestFixture { private Trigger trigger; public override void TestSetup() { trigger = new Trigger(); } public void NullSubscription_Throws() { CheckThrow(typeof(ArgumentNullException)); trigger.Restore(null, wait: 1); } public void AppendsTheSubscriptionWithItsWaitNumber() { var wait = new TriggerWait(capacity: 1); trigger.Restore(wait.Subscriptions[0], wait: 7); CheckEqual(1, trigger.SubscriptionCount); CheckTrue(ReferenceEquals(wait.Subscriptions[0], trigger.SubscriptionAt(0))); CheckEqual(7, trigger.WaitAt(0)); } public void SubscriptionsWhoseWaitsAreNotFilledYet_AreAllKept() { for (int i = 0; i < 100; ++i) { trigger.Restore(new TriggerWait(capacity: 1).Subscriptions[0], wait: 5); } CheckEqual(100, trigger.SubscriptionCount); } } public class EnsureSubscribedTests : TestFixture { private List woken; private Trigger trigger; public override void TestSetup() { woken = new List(); trigger = new Trigger(); } public void NullSubscription_Throws() { CheckThrow(typeof(ArgumentNullException)); trigger.EnsureSubscribed(null, wait: 1); } public void SubscriptionTheTriggerLacks_IsAdded() { var wait = new TriggerWait(capacity: 1); trigger.EnsureSubscribed(wait.Subscriptions[0], wait: 1); CheckEqual(1, trigger.SubscriptionCount); CheckTrue(ReferenceEquals(wait.Subscriptions[0], trigger.SubscriptionAt(0))); CheckEqual(1, trigger.WaitAt(0)); } public void SubscriptionTheTriggerHolds_IsNotAddedAgain() { TriggerWait wait = WaitOf(woken, order: 0, trigger); trigger.EnsureSubscribed(wait.Subscriptions[0], wait.Wait); CheckEqual(1, trigger.SubscriptionCount); } public void SubscriptionHeldForAnEarlierWait_IsAddedForThisOne() { TriggerWait wait = WaitOf(woken, order: 0, trigger); wait.Begin(new Frame { Wake = wait.Slot }, requiresAll: false); trigger.EnsureSubscribed(wait.Subscriptions[0], wait.Wait); CheckEqual(2, trigger.SubscriptionCount); CheckEqual(wait.Wait, trigger.WaitAt(1)); } public void SubscriptionOfADroppedSlot_IsNotAddedAgain() { TriggerWait wait = WaitOf(woken, order: 0, trigger); wait.Slot.Dropped = true; trigger.EnsureSubscribed(wait.Subscriptions[0], wait.Wait); CheckEqual(1, trigger.SubscriptionCount); } public void SubscriptionRestoredFromASave_IsNotAddedAgain() { var wait = new TriggerWait(capacity: 1); trigger.Restore(wait.Subscriptions[0], wait: 3); trigger.EnsureSubscribed(wait.Subscriptions[0], wait: 3); CheckEqual(1, trigger.SubscriptionCount); } public void SubscriptionUsedUpByARaise_IsAddedAgain() { TriggerWait wait = WaitOf(woken, order: 0, trigger); trigger.Raise(); trigger.EnsureSubscribed(wait.Subscriptions[0], wait.Wait); CheckEqual(1, trigger.SubscriptionCount); } public void SubscriptionSweptOutOfTheList_IsAddedAgain() { TriggerWait wait = WaitOf(woken, order: 0, trigger); wait.Slot.Dropped = true; for (int i = 0; i < 20; ++i) { WaitOf(woken, order: 1, trigger); } int before = trigger.SubscriptionCount; trigger.EnsureSubscribed(wait.Subscriptions[0], wait.Wait); CheckEqual(before + 1, trigger.SubscriptionCount); } public void SubscriptionHeldByAnotherTrigger_IsAddedHere() { TriggerWait wait = WaitOf(woken, order: 0, new Trigger()); trigger.EnsureSubscribed(wait.Subscriptions[0], wait.Wait); CheckEqual(1, trigger.SubscriptionCount); } public void EarlierEntryOfTheSubscriptionSweptAway_LeavesTheLaterOneHeld() { TriggerWait wait = WaitOf(woken, order: 0, trigger); wait.Begin(new Frame { Wake = wait.Slot }, requiresAll: false); wait.Add(trigger); for (int i = 0; i < 20; ++i) { WaitOf(woken, order: 1, trigger); } int before = trigger.SubscriptionCount; trigger.EnsureSubscribed(wait.Subscriptions[0], wait.Wait); CheckEqual(before, trigger.SubscriptionCount); } } public class RaiseTests : TestFixture { private List woken; private Trigger trigger; [IgnoreTest] public void SubscribeAll(IEnumerable orders) { foreach (int order in orders) { WaitOf(woken, order, trigger); } } public override void TestSetup() { woken = new List(); trigger = new Trigger(); } public void NobodySubscribed_DoesNothing() { trigger.Raise(); CheckEqual(0, woken.Count); CheckEqual(0, trigger.SubscriptionCount); } public void WakesEverySubscriber() { SubscribeAll(new[] { 0, 1, 2 }); trigger.Raise(); CheckEqual(new[] { 0, 1, 2 }, woken.ToArray()); } public void SubscriptionsMadeInOrder_WakeInThatOrder() { SubscribeAll(Counting(100)); trigger.Raise(); CheckEqual(Counting(100), woken.ToArray()); } public void FewSubscribersAddedBackwards_WakeInRegistrationOrder() { SubscribeAll(new[] { 2, 1, 0 }); trigger.Raise(); CheckEqual(new[] { 0, 1, 2 }, woken.ToArray()); } public void SubscribersAddedBackwards_AreReleasedAfterTheRaise() { TriggerWait later = WaitOf(woken, order: 1, trigger); TriggerWait earlier = WaitOf(woken, order: 0, trigger); earlier.Begin(new Frame { Wake = earlier.Slot }, requiresAll: false); earlier.Add(trigger); trigger.Raise(); CheckNull(later.Subscriptions[0].HeldBy); CheckNull(earlier.Subscriptions[0].HeldBy); } public void ManySubscribersAddedBackwards_WakeInRegistrationOrder() { int[] backwards = Counting(100); Array.Reverse(backwards); SubscribeAll(backwards); trigger.Raise(); CheckEqual(Counting(100), woken.ToArray()); } public void ManySubscribersAddedEvensThenOdds_WakeInRegistrationOrder() { var shuffled = new List(); for (int order = 0; order < 100; order += 2) { shuffled.Add(order); } for (int order = 1; order < 100; order += 2) { shuffled.Add(order); } SubscribeAll(shuffled); trigger.Raise(); CheckEqual(Counting(100), woken.ToArray()); } public void SubscribersWithTheSameOrder_WakeInTheOrderTheySubscribed() { foreach (int tag in new[] { 5, 3, 9 }) { WaitOf(woken, order: 7, tag: tag, requiresAll: false, triggers: new[] { trigger }); } WaitOf(woken, order: 2, tag: 1, requiresAll: false, triggers: new[] { trigger }); trigger.Raise(); CheckEqual(new[] { 1, 5, 3, 9 }, woken.ToArray()); } public void Raise_ConsumesTheSubscriptions() { SubscribeAll(new[] { 0, 1 }); trigger.Raise(); CheckEqual(0, trigger.SubscriptionCount); trigger.Raise(); CheckEqual(new[] { 0, 1 }, woken.ToArray()); } public void WaitThatBeganAgain_IsNotMarkedByTheOldSubscription() { var other = new Trigger(); TriggerWait wait = WaitOf(woken, order: 0, trigger); wait.Begin(new Frame { Wake = wait.Slot }, requiresAll: false); wait.Add(other); trigger.Raise(); CheckEqual(0, wait.Seen); CheckEqual(0, woken.Count); } public void WaitForAny_IsWokenByTheFirstRaiseOnly() { var other = new Trigger(); TriggerWait wait = WaitOf(woken, order: 0, tag: 0, requiresAll: false, triggers: new[] { trigger, other }); other.Raise(); trigger.Raise(); CheckEqual(new[] { 0 }, woken.ToArray()); CheckEqual(1, wait.Fired); CheckEqual(0, trigger.SubscriptionCount); } public void WaitForAll_IsWokenByTheLastRaise() { var other = new Trigger(); TriggerWait wait = WaitOf(woken, order: 0, tag: 0, requiresAll: true, triggers: new[] { trigger, other }); trigger.Raise(); CheckEqual(0, woken.Count); other.Raise(); CheckEqual(new[] { 0 }, woken.ToArray()); CheckEqual(1, wait.Fired); } public void WaitForAllOfOneTriggerTwice_IsWokenByOneRaise() { TriggerWait wait = WaitOf(woken, order: 0, tag: 0, requiresAll: true, triggers: new[] { trigger, trigger }); trigger.Raise(); CheckEqual(new[] { 0 }, woken.ToArray()); CheckEqual(1, wait.Fired); } public void DroppedSlot_IsNotWoken() { TriggerWait dropped = WaitOf(woken, order: 0, trigger); dropped.Slot.Dropped = true; SubscribeAll(new[] { 1 }); trigger.Raise(); CheckEqual(new[] { 1 }, woken.ToArray()); CheckEqual(0, dropped.Seen); } public void UnattachedWait_ThrowsAndLeavesEverySubscriptionUnmarked() { TriggerWait unattached = WaitOf(woken, order: 0, trigger); unattached.Slot = null; TriggerWait attached = WaitOf(woken, order: 1, trigger); bool threw = false; try { trigger.Raise(); } catch (InvalidOperationException) { threw = true; } CheckTrue(threw); CheckEqual(0, attached.Seen); CheckEqual(0, woken.Count); CheckEqual(2, trigger.SubscriptionCount); } public void WaitAttachedAfterwards_IsWokenByTheNextRaise() { TriggerWait wait = WaitOf(woken, order: 4, trigger); WakeSlot slot = wait.Slot; wait.Slot = null; try { trigger.Raise(); } catch (InvalidOperationException) { } wait.Slot = slot; trigger.Raise(); CheckEqual(new[] { 4 }, woken.ToArray()); } } public class SchedulerTests : TestFixture { private SchedulerWorld world; private ExceptionSandbox sandbox; private ArcScheduler scheduler; private Trigger trigger; public override void TestSetup() { world = new SchedulerWorld(); sandbox = TestTicks.NewSandbox(); scheduler = new ArcScheduler(); trigger = new Trigger(); } public void Raise_WakesASuspendedArcOnTheNextTick() { var waiter = new SuspenderArc(); scheduler.Add(waiter); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0)); waiter.WaitOn(trigger); trigger.Raise(); CheckEqual(0, scheduler.SuspendedCount); CheckEqual(1, waiter.Resumes); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0.016)); CheckEqual(2, waiter.Resumes); } public void Raise_DoesNotWakeAnArcThatIsNotSubscribed() { var waiter = new SuspenderArc(); scheduler.Add(waiter); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0)); trigger.Raise(); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0.016)); CheckEqual(1, waiter.Resumes); CheckEqual(1, scheduler.SuspendedCount); } public void ArcThatWaitsOnTheTriggerEachTime_IsWokenByEveryRaise() { var waiter = new SuspenderArc { Triggers = new[] { trigger } }; scheduler.Add(waiter); for (int frame = 0; frame < 3; ++frame) { scheduler.Tick(world, ArcSchedulerTests.At(sandbox, frame)); trigger.Raise(); } scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 3)); CheckEqual(4, waiter.Resumes); } public void WaitForAny_IsWokenByTheFirstTriggerAndNamesIt() { var other = new Trigger(); var waiter = new SuspenderArc(); scheduler.Add(waiter); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0)); waiter.WaitOn(trigger, other); other.Raise(); trigger.Raise(); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 1)); CheckEqual(2, waiter.Resumes); CheckTrue(ReferenceEquals(other, waiter.Waiting.Result)); } public void WaitForAny_IsNotWokenAgainByTheOtherTrigger() { var other = new Trigger(); var waiter = new SuspenderArc(); scheduler.Add(waiter); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0)); waiter.WaitOn(trigger, other); trigger.Raise(); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 1)); other.Raise(); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 2)); CheckEqual(2, waiter.Resumes); CheckEqual(1, scheduler.SuspendedCount); } public void WaitForAll_IsWokenOnlyByTheLastTrigger() { var other = new Trigger(); var waiter = new SuspenderArc { RequireAll = true }; scheduler.Add(waiter); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0)); waiter.WaitOn(trigger, other); trigger.Raise(); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 1)); CheckEqual(1, waiter.Resumes); other.Raise(); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 2)); CheckEqual(2, waiter.Resumes); CheckTrue(ReferenceEquals(other, waiter.Waiting.Result)); } public void Raise_WakesAnArcSleepingOnATimeoutBeforeTheDeadline() { var waiter = new SuspenderArc { Deadline = TimeSpan.FromSeconds(100) }; scheduler.Add(waiter); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0)); CheckEqual(1, scheduler.SleepingCount); waiter.WaitOn(trigger); trigger.Raise(); CheckEqual(0, scheduler.SleepingCount); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 1)); CheckEqual(2, waiter.Resumes); } public void RemovedArc_IsNotWoken() { var waiter = new SuspenderArc(); scheduler.Add(waiter); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0)); waiter.WaitOn(trigger); scheduler.Remove(waiter); trigger.Raise(); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 1)); CheckEqual(1, waiter.Resumes); CheckEqual(0, scheduler.Count); CheckEqual(0, trigger.SubscriptionCount); } public void ArcThatWaitedAgainOnAnotherTrigger_IsNotWokenByTheFirst() { var other = new Trigger(); var waiter = new SuspenderArc(); scheduler.Add(waiter); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 0)); waiter.WaitOn(trigger); waiter.WaitOn(other); trigger.Raise(); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 1)); CheckEqual(1, waiter.Resumes); other.Raise(); scheduler.Tick(world, ArcSchedulerTests.At(sandbox, 2)); CheckEqual(2, waiter.Resumes); } public void ArcOutsideAScheduler_CannotWaitOnATrigger() { var waiter = new SuspenderArc { Triggers = new[] { trigger } }; waiter.Tick(world, ArcSchedulerTests.At(sandbox, 0)); CheckEqual(1, sandbox.Entries.Count); CheckEqual(typeof(InvalidOperationException), sandbox.Entries[0].Exception.GetType()); } } public class RaiseAllocationTests : TestFixture { private Trigger trigger; private TriggerWait[] waits; private Frame[] frames; [IgnoreTest] public void WaitAll() { for (int i = 0; i < waits.Length; ++i) { waits[i].Begin(frames[i], requiresAll: false); waits[i].Add(trigger); } } // One round first, so everything a raise grows is ready for the next. public override void TestSetup() { trigger = new Trigger(); waits = new TriggerWait[100]; frames = new Frame[waits.Length]; for (int i = 0; i < waits.Length; ++i) { waits[i] = new TriggerWait(capacity: 1); frames[i] = new Frame { Wake = new WakeSlot { Order = waits.Length - 1 - i } }; } WaitAll(); trigger.Raise(); } [AllocationTest] public void RaiseOfSubscribersAddedOutOfOrder_AllocatesNothing() { WaitAll(); trigger.Raise(); CheckNoAllocations(); } } } }