From 858091d6686ee4ee2d0cc0339712f20d177c0403 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 11 Apr 2021 04:56:08 +0700 Subject: [PATCH] Add signal-slot functionality to Acedia --- sources/Events/Signal.uc | 347 +++++++++++++++++++ sources/Events/Slot.uc | 151 ++++++++ sources/Events/Tests/MockSignal.uc | 43 +++ sources/Events/Tests/MockSignalSlotClient.uc | 49 +++ sources/Events/Tests/MockSlot.uc | 41 +++ sources/Events/Tests/TEST_SignalsSlots.uc | 214 ++++++++++++ sources/Manifest.uc | 35 +- 7 files changed, 863 insertions(+), 17 deletions(-) create mode 100644 sources/Events/Signal.uc create mode 100644 sources/Events/Slot.uc create mode 100644 sources/Events/Tests/MockSignal.uc create mode 100644 sources/Events/Tests/MockSignalSlotClient.uc create mode 100644 sources/Events/Tests/MockSlot.uc create mode 100644 sources/Events/Tests/TEST_SignalsSlots.uc diff --git a/sources/Events/Signal.uc b/sources/Events/Signal.uc new file mode 100644 index 0000000..3f683d1 --- /dev/null +++ b/sources/Events/Signal.uc @@ -0,0 +1,347 @@ +/** + * One of the two classes that make up a core of event system in Acedia. + * `Signal`s, along with `Slot`s, are used for communication between + * objects. Signals can be connected to slots of appropriate class and emitted. + * When a signal is emitted, all connected slots are notified and their handler + * is called. + * This `Signal`-`Slot` system is essentially a wrapper for delegates + * (`Slot` wraps over a single delegate, allowing us to store them in array), + * but, unlike them, makes it possible to add several handlers for any event in + * a convenient to use way, e.g..: + * `_.unreal.OnTick(self).connect = myTickHandler` + * To create your own `Signal` you need to: + * 1. Make a non-abstract child class of `Signal`; + * 2. Use one of the templates presented in this file below; + * 3. Create a paired `Slot` class and set it's class to `relatedSlotClass` + * in `defaultproperties`. + * 4. (Recommended) Provide a standard interface by defining an event + * method (similar to `_.unreal.OnTick()`) in an object that will own + * this signal, example of definition is also listed below. + * More detailed information can be found in documentation. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class Signal extends AcediaObject + abstract; + +// Class of the slot that can catch your `Signal` class +var public const class relatedSlotClass; + +// We want to always return a non-`none` slot to avoid "Access 'none'" +// errors. +// So if provided signal receiver is invalid - we still create a `Slot` +// for it, but remember it in this array of slots we aren't going to use +// in order to dispose of them later. +var private array failedSlots; + +// Set to `true` when we are in the process of deleting our `Slot`s. +// Once `Slot` is deallocated, it notifies it's `Signal` (us) that it +// should be removed. +// But if it's deallocated because we are removing it we want to ignore +// their notification and this flag helps us do that. +var private bool doingSelfCleaning; +// Used to provide iterating interface (`StartIterating()` / `GetNextSlot()`). +// Points at the next slot to return. +var private int nextSlotIndex; + +// These arrays could be defined as one array of `struct`s with four +// elements. +// We use four different arrays instead for performance reasons. +// They must have the same length at all times and elements with the +// same index correspond to the same "record". + +// Reference to registered `Slot` +var private array registeredSlots; +// Life version of the registered `Slot`, to track unexpected deallocations +var private array slotLifeVersions; +// Receiver, associated with the `Slot`: when it's deallocated, +// corresponding `Slot` should be removed +var private array slotReceivers; +// Life version of the registered receiver, to track it's deallocation +var private array slotReceiversLifeVersions; + +/* TEMPLATE for handlers without returned values: + +public final function Emit() +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + (nextSlot).connect(); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} +*/ + +/* TEMPLATE for handlers with returned values: + +public final function int Emit() +{ + local newValue; + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + newValue = (nextSlot).connect(); + // This check if necessary before using returned value + if (!nextSlot.IsEmpty()) + { + // Now handle `newValue` however you see fit + } + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); + return value; +} +*/ + +/* TEMPLATE for the interface method: + +var private mySignal; +public final function OnMyEvent(AcediaObject receiver) +{ + return (mySignal.NewSlot(receiver)); +} +*/ + +protected function Finalizer() +{ + doingSelfCleaning = true; + _.memory.FreeMany(registeredSlots); + doingSelfCleaning = false; + registeredSlots.length = 0; + slotLifeVersions.length = 0; + slotReceivers.length = 0; + slotReceiversLifeVersions.length = 0; +} + +/** + * Creates a new slot for `receiver` to catch emitted signals. + * Supposed to be used inside a special interface method only. + * + * @param receiver Receiver to which new `Slot` would be connected. + * Method connected to a `Slot` generated by this method must belong to + * the `receiver`, otherwise behavior of `Signal`-`Slot` system is + * undefined. + * Must be a properly allocated `AcediaObject`. + * @return New `Slot` object that will be connected to the caller `Signal` if + * provided `receiver` is correct. Guaranteed to have class + * `relatedSlotClass`. + */ +public final function Slot NewSlot(AcediaObject receiver) +{ + local Slot newSlot; + newSlot = Slot(_.memory.Allocate(relatedSlotClass)); + newSlot.Initialize(self); + AddSlot(newSlot, receiver); + return newSlot; +} + +/** + * Disconnects all of the `receiver`'s `Slot`s from the caller `Signal`. + * + * @param receiver Object to disconnect from the caller `Signal`. + */ +public final function Disconnect(AcediaObject receiver) +{ + local int i; + doingSelfCleaning = true; + while (i < slotReceivers.length) + { + if (slotReceivers[i] == none || slotReceivers[i] == receiver) + { + _.memory.Free(registeredSlots[i]); + registeredSlots.Remove(i, 1); + slotLifeVersions.Remove(i, 1); + slotReceivers.Remove(i, 1); + slotReceiversLifeVersions.Remove(i, 1); + } + else { + i += 1; + } + } + doingSelfCleaning = false; +} + +/** + * Adds new `Slot` `newSlot` with receiver `receiver` to the caller `Signal`. + * + * Does nothing if `newSlot` is already added to the caller `Signal`. + * + * @param newSlot Slot to add. Must be initialize for the caller `Signal`. + * @param receiver Receiver to which new `Slot` would be connected. + * Method connected to a `Slot` generated by this method must belong to + * the `receiver`, otherwise behavior of `Signal`-`Slot` system is + * undefined. + * Must be a properly allocated `AcediaObject`. + */ +protected final function AddSlot(Slot newSlot, AcediaObject receiver) +{ + local int i; + local int newSlotIndex; + if (newSlot == none) return; + if (newSlot.class != relatedSlotClass) return; + if (!newSlot.IsOwnerSignal(self)) return; + if (receiver == none || !receiver.IsAllocated()) + { + failedSlots[failedSlots.length] = newSlot; + return; + } + for (i = 0; i < registeredSlots.length; i += 1) + { + if (registeredSlots[i] != newSlot) { + continue; + } + if (slotLifeVersions[i] != newSlot.GetLifeVersion()) + { + slotLifeVersions[i] = newSlot.GetLifeVersion(); + slotReceivers[i] = receiver; + if (receiver != none) { + slotReceiversLifeVersions[i] = receiver.GetLifeVersion(); + } + } + return; + } + newSlotIndex = registeredSlots.length; + registeredSlots[newSlotIndex] = newSlot; + slotLifeVersions[newSlotIndex] = newSlot.GetLifeVersion(); + slotReceivers[newSlotIndex] = receiver; + slotReceiversLifeVersions[newSlotIndex] = receiver.GetLifeVersion(); +} + +/** + * Removes given `slotToRemove` if it was connected to the caller `Signal`. + * + * Does not deallocate `slotToRemove`. + * + * Cannot fail. + * + * @param slotToRemove Slot to be removed. + */ +public final function RemoveSlot(Slot slotToRemove) +{ + local int i; + if (slotToRemove == none) return; + if (doingSelfCleaning) return; + + for (i = 0; i < registeredSlots.length; i += 1) + { + if (registeredSlots[i] == slotToRemove) + { + registeredSlots.Remove(i, 1); + slotLifeVersions.Remove(i, 1); + slotReceivers.Remove(i, 1); + slotReceiversLifeVersions.Remove(i, 1); + return; + } + } +} + +/** + * One of two methods that provide an iterator access to the private array of + * `Slot`s and perform all the necessary safety checks. + * + * Must be called before each new iterating cycle. + * + * There cannot be any `Slot` additions or removal during one iteration cycle. + */ +protected final function StartIterating() +{ + nextSlotIndex = 0; +} + +/** + * One of two methods that provide an iterator access to the private array of + * `Slot`s and perform all the necessary safety checks. + * + * `StartIterating()` must be called to initialize iteration cycle, then this + * method can be called until it returns `none`. + * + * There cannot be any `Slot` additions or removal during one iteration cycle. + * + * @return Next `Slot` that must receive emitted signal. `none` means that + * there are no more `Slot`s to iterate over. + */ +protected final function Slot GetNextSlot() +{ + local bool isNextSlotValid; + local int nextSlotLifeVersion, nextReceiverLifeVersion; + local Slot nextSlot; + local AcediaObject nextReceiver; + doingSelfCleaning = true; + while (nextSlotIndex < registeredSlots.length) + { + nextSlot = registeredSlots[nextSlotIndex]; + nextSlotLifeVersion = slotLifeVersions[nextSlotIndex]; + nextReceiver = slotReceivers[nextSlotIndex]; + nextReceiverLifeVersion = slotReceiversLifeVersions[nextSlotIndex]; + isNextSlotValid = (nextSlot.GetLifeVersion() == nextSlotLifeVersion) + && (nextReceiver.GetLifeVersion() == nextReceiverLifeVersion); + if (isNextSlotValid) + { + nextSlotIndex += 1; + return nextSlot; + } + else + { + registeredSlots.Remove(nextSlotIndex, 1); + slotLifeVersions.Remove(nextSlotIndex, 1); + slotReceivers.Remove(nextSlotIndex, 1); + slotReceiversLifeVersions.Remove(nextSlotIndex, 1); + _.memory.Free(nextSlot); + } + } + doingSelfCleaning = false; + return none; +} + +/** + * In case it's detected that some of the slots do not actually have any + * handler setup - this method will clean them up. + */ +protected final function CleanEmptySlots() +{ + local int index; + _.memory.FreeMany(failedSlots); + failedSlots.length = 0; + doingSelfCleaning = true; + while (index < registeredSlots.length) + { + if (registeredSlots[index].IsEmpty()) + { + registeredSlots[index].FreeSelf(slotLifeVersions[index]); + _.memory.Free(registeredSlots[index]); + registeredSlots.Remove(index, 1); + slotLifeVersions.Remove(index, 1); + slotReceivers.Remove(index, 1); + slotReceiversLifeVersions.Remove(index, 1); + } + else { + index += 1; + } + } + doingSelfCleaning = false; +} + +defaultproperties +{ + relatedSlotClass = class'Slot' +} \ No newline at end of file diff --git a/sources/Events/Slot.uc b/sources/Events/Slot.uc new file mode 100644 index 0000000..2a72ce6 --- /dev/null +++ b/sources/Events/Slot.uc @@ -0,0 +1,151 @@ +/** + * One of the two classes that make up a core of event system in Acedia. + * `Signal`s, along with `Slot`s, are used for communication between + * objects. Signals can be connected to slots of appropriate class and emitted. + * When a signal is emitted, all connected slots are notified and their handler + * is called. + * This `Signal`-`Slot` system is essentially a wrapper for delegates + * (`Slot` wraps over a single delegate, allowing us to store them in array), + * but, unlike them, makes it possible to add several handlers for any event in + * a convenient to use way, e.g.: + * `_.unreal.OnTick(self).connect = myTickHandler` + * To create your own `Slot` you need to: + * 1. Make a non-abstract child class of `Signal`; + * 2. Use one of the templates presented in this file below. + * More detailed information can be found in documentation. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class Slot extends AcediaObject + abstract; + +var private bool dummyMethodCalled; +var private Signal ownerSignal; + +/* TEMPLATE for handlers without returned values: +delegate connect() +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} +*/ + +/* TEMPLATE for handlers with returned values: +delegate connect() +{ + DummyCall(); + // Return anything you want: + // this value will be filtered inside corresponding `Signal` + return ; +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} +*/ + +protected function Finalizer() +{ + dummyMethodCalled = false; + if (ownerSignal != none) { + ownerSignal.RemoveSlot(self); + } + ownerSignal = none; +} + +/** + * Calling this method marks caller `Slot` as "empty", i.e. having an empty + * delegate. `Slot`s like that are deleted from `Signal`s upon detection. + * + * Must be called inside your `connect()` implementation. + */ +protected final function DummyCall() +{ + dummyMethodCalled = true; + // We do not want to call `ownerSignal.RemoveSlot(self)` here, since + // `ownerSignal` is likely in process of iterating through it's `Slot`s + // and removing (or adding) `Slot`s from it can mess up that process. +} + +/** + * Initialized caller `Slot` to receive signals emitted by `newOwnerSignal`. + * + * Can only be done once for every `Slot`. + * + * @param newOwnerSignal `Signal` we want to receive emitted signals from. + * @return `true` if initialization was successful and `false` otherwise + * (if `newOwnerSignal` is invalid or caller `Slot` was + * already initialized). + */ +public final function bool Initialize(Signal newOwnerSignal) +{ + if (ownerSignal != none) { + return false; + } + if (newOwnerSignal == none || !newOwnerSignal.IsAllocated()) + { + FreeSelf(); + return false; + } + ownerSignal = newOwnerSignal; + return true; +} + +/** + * Checks if caller `Slot` was initialized to receive `testSignal`'s signals. + * + * @param testSignal `Signal` to test. + * @return `true` if caller `Slot` was initialized to receiver `testSignal`'s + * signals and `false` otherwise. + */ +public final function bool IsOwnerSignal(Signal testSignal) +{ + return (ownerSignal == testSignal); +} + +/** + * Checks if caller `Slot` was detected to be "empty", i.e. having an empty + * delegate. `Slot`s like that are deleted from `Signal`s upon detection. + * + * @return `true` if caller `Slot` is empty (and should be removed from the + * appropriate `Signal`) and `false` otherwise. + */ +public final function bool IsEmpty() +{ + return dummyMethodCalled; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Events/Tests/MockSignal.uc b/sources/Events/Tests/MockSignal.uc new file mode 100644 index 0000000..5b872cb --- /dev/null +++ b/sources/Events/Tests/MockSignal.uc @@ -0,0 +1,43 @@ +/** + * `Signal` class intended for testing signal/slot functionality of Acedia. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class MockSignal extends Signal; + +public final function int Emit(int value, optional bool mod) +{ + local int newValue; + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + newValue = MockSlot(nextSlot).connect(value, mod); + if (!nextSlot.IsEmpty()) { + value = newValue; + } + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); + return value; +} + +defaultproperties +{ + relatedSlotClass = class'MockSlot' +} \ No newline at end of file diff --git a/sources/Events/Tests/MockSignalSlotClient.uc b/sources/Events/Tests/MockSignalSlotClient.uc new file mode 100644 index 0000000..dbfdf69 --- /dev/null +++ b/sources/Events/Tests/MockSignalSlotClient.uc @@ -0,0 +1,49 @@ +/** + * Object that provides a signal handler for testing signal/slot functionality + * of Acedia. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class MockSignalSlotClient extends AcediaObject; + +var private int value; + +public final function SetValue(int newValue) +{ + value = newValue; +} + +// Return `SMockSlot` for testing purposes +public final function MockSlot AddToSignal(MockSignal signal) +{ + local MockSlot slot; + slot = MockSlot(signal.NewSlot(self)); + slot.connect = Handler; + return slot; +} + +private final function int Handler(int inputValue, optional bool doSubtract) +{ + if (doSubtract) { + return inputValue - value; + } + return inputValue + value; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Events/Tests/MockSlot.uc b/sources/Events/Tests/MockSlot.uc new file mode 100644 index 0000000..874f8f7 --- /dev/null +++ b/sources/Events/Tests/MockSlot.uc @@ -0,0 +1,41 @@ +/** + * `Slot` class intended for testing signal/slot functionality of Acedia. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class MockSlot extends Slot; + +delegate int connect(int value, optional bool mod) +{ + DummyCall(); + return 13; +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Events/Tests/TEST_SignalsSlots.uc b/sources/Events/Tests/TEST_SignalsSlots.uc new file mode 100644 index 0000000..6920b6c --- /dev/null +++ b/sources/Events/Tests/TEST_SignalsSlots.uc @@ -0,0 +1,214 @@ +/** + * Set of tests for signal/slot functionality of Acedia. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class TEST_SignalsSlots extends TestCase + abstract; + +protected static function TESTS() +{ + Context("Testing regularly connecting and disconnecting slots to" + @ "a signal."); + Test_Connecting(); + Test_Disconnecting(); + Context("Testing how signals and slots system handles deallocations and" + @ "unexpected changes to managed objects."); + Test_DeallocSlots(); + Test_EmptySlots(); + Test_DeallocReceivers(); +} + +protected static function Test_Connecting() +{ + local int i; + local MockSignal signal; + local MockSignalSlotClient nextObject; + local array objects; + Issue("Slots are not connected correctly."); + signal = MockSignal(__().memory.Allocate(class'MockSignal')); + for (i = 0; i < 100; i += 1) + { + nextObject = MockSignalSlotClient( + __().memory.Allocate(class'MockSignalSlotClient')); + objects[objects.length] = nextObject; + nextObject.AddToSignal(signal); + nextObject.SetValue(i + 1); + } + // 1 + ... + 100 = 100 * 101 / 2 = 5050 + // 5050 + 100 = 5150 + TEST_ExpectTrue(signal.Emit(100) == 5150); + // -5050 + 20 = -5030 + TEST_ExpectTrue(signal.Emit(20, true) == -5030); + __().memory.Free(signal); + + Issue("Several slots from the same object are not connected correctly."); + signal = MockSignal(__().memory.Allocate(class'MockSignal')); + // Object with `SetValue(100)` + nextObject.AddToSignal(signal); + nextObject.AddToSignal(signal); + nextObject.AddToSignal(signal); + TEST_ExpectTrue(signal.Emit(400, true) == 100); + __().memory.FreeMany(objects); +} + +protected static function Test_Disconnecting() +{ + local int i; + local MockSignal signal; + local MockSignalSlotClient nextObject; + local array objects; + Issue("Slots are not disconnected correctly."); + signal = MockSignal(__().memory.Allocate(class'MockSignal')); + for (i = 0; i < 100; i += 1) + { + nextObject = MockSignalSlotClient( + __().memory.Allocate(class'MockSignalSlotClient')); + objects[objects.length] = nextObject; + nextObject.AddToSignal(signal); + nextObject.SetValue(i + 1); + } + // Now disconnect the ones with odd values + for (i = 0; i < 100; i += 2) { + signal.Disconnect(objects[i]); // value is `i + 1`, so 1, 3, 5,... + } + // 2 + 4 + 6 + ... + 100 = 2 * (1 + ... + 50) = 50 * 51 = 2550 + // 2550 + 50 = 2600 + TEST_ExpectTrue(signal.Emit(50) == 2600); + // -2550 + 550 = -2000 + TEST_ExpectTrue(signal.Emit(550, true) == -2000); + __().memory.Free(signal); + __().memory.FreeMany(objects); +} + +protected static function Test_DeallocSlots() +{ + local int i; + local MockSignal signal; + local MockSignalSlotClient nextObject; + local array slots; + local array objects; + Issue("Deallocated slots are still being called."); + signal = MockSignal(__().memory.Allocate(class'MockSignal')); + for (i = 0; i < 100; i += 1) + { + nextObject = MockSignalSlotClient( + __().memory.Allocate(class'MockSignalSlotClient')); + objects[objects.length] = nextObject; + slots[slots.length] = nextObject.AddToSignal(signal); + nextObject.SetValue(i + 1); + } + // Now disconnect the ones with odd values + for (i = 0; i < 100; i += 2) { + slots[i].FreeSelf(); // value is `i + 1`, so 1, 3, 5,... + } + // 2 + 4 + 6 + ... + 100 = 2 * (1 + ... + 50) = 50 * 51 = 2550 + // 2550 + 50 = 2600 + TEST_ExpectTrue(signal.Emit(50) == 2600); + // -2550 + 550 = -2000 + TEST_ExpectTrue(signal.Emit(550, true) == -2000); + __().memory.Free(signal); + __().memory.FreeMany(objects); +} + +protected static function Test_EmptySlots() +{ + local int i; + local bool slotsAreNotDeallocated; + local MockSignal signal; + local MockSignalSlotClient nextObject; + local array slots; + local array objects; + Issue("Slots with emptied delegates are still being called."); + signal = MockSignal(__().memory.Allocate(class'MockSignal')); + for (i = 0; i < 100; i += 1) + { + nextObject = MockSignalSlotClient( + __().memory.Allocate(class'MockSignalSlotClient')); + objects[objects.length] = nextObject; + slots[slots.length] = nextObject.AddToSignal(signal); + nextObject.SetValue(i + 1); + } + // Now disconnect the ones with odd values + for (i = 0; i < 100; i += 2) { + slots[i].connect = none; // value is `i + 1`, so 1, 3, 5,... + } + // 2 + 4 + 6 + ... + 100 = 2 * (1 + ... + 50) = 50 * 51 = 2550 + // 2550 + 50 = 2600 + TEST_ExpectTrue(signal.Emit(50) == 2600); + // -2550 + 550 = -2000 + TEST_ExpectTrue(signal.Emit(550, true) == -2000); + for (i = 0; i < 100; i += 2) + { + if (slots[i].IsAllocated()) + { + slotsAreNotDeallocated = true; + break; + } + } + Issue("Slots with emptied delegates are not deallocated."); + TEST_ExpectFalse(slotsAreNotDeallocated); + __().memory.Free(signal); + __().memory.FreeMany(objects); +} + +protected static function Test_DeallocReceivers() +{ + local int i; + local bool slotsAreNotDeallocated; + local MockSignal signal; + local MockSignalSlotClient nextObject; + local array slots; + local array objects; + Issue("Deallocated receivers still receive messages."); + signal = MockSignal(__().memory.Allocate(class'MockSignal')); + for (i = 0; i < 100; i += 1) + { + nextObject = MockSignalSlotClient( + __().memory.Allocate(class'MockSignalSlotClient')); + objects[objects.length] = nextObject; + slots[slots.length] = nextObject.AddToSignal(signal); + nextObject.SetValue(i + 1); + } + // Now disconnect the ones with odd values + for (i = 0; i < 100; i += 2) { + objects[i].FreeSelf(); // value is `i + 1`, so 1, 3, 5,... + } + // 2 + 4 + 6 + ... + 100 = 2 * (1 + ... + 50) = 50 * 51 = 2550 + // 2550 + 50 = 2600 + TEST_ExpectTrue(signal.Emit(50) == 2600); + // -2550 + 550 = -2000 + TEST_ExpectTrue(signal.Emit(550, true) == -2000); + for (i = 0; i < 100; i += 2) + { + if (slots[i].IsAllocated()) + { + slotsAreNotDeallocated = true; + break; + } + } + Issue("Slots with deallocated receivers are not deallocated."); + TEST_ExpectFalse(true); + __().memory.Free(signal); + __().memory.FreeMany(objects); +} + +defaultproperties +{ + caseGroup = "Events" + caseName = "Signals and slots" +} \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 4cb36f0..7b60c6c 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -34,21 +34,22 @@ defaultproperties testCases(0) = class'TEST_Base' testCases(1) = class'TEST_Boxes' testCases(2) = class'TEST_Refs' - testCases(3) = class'TEST_UnrealAPI' - testCases(4) = class'TEST_Aliases' - testCases(5) = class'TEST_ColorAPI' - testCases(6) = class'TEST_Text' - testCases(7) = class'TEST_TextAPI' - testCases(8) = class'TEST_Parser' - testCases(9) = class'TEST_JSON' - testCases(10) = class'TEST_TextCache' - testCases(11) = class'TEST_User' - testCases(12) = class'TEST_Memory' - testCases(13) = class'TEST_DynamicArray' - testCases(14) = class'TEST_AssociativeArray' - testCases(15) = class'TEST_CollectionsMixed' - testCases(16) = class'TEST_Iterator' - testCases(17) = class'TEST_Command' - testCases(18) = class'TEST_CommandDataBuilder' - testCases(19) = class'TEST_LogMessage' + testCases(3) = class'TEST_SignalsSlots' + testCases(4) = class'TEST_UnrealAPI' + testCases(5) = class'TEST_Aliases' + testCases(6) = class'TEST_ColorAPI' + testCases(7) = class'TEST_Text' + testCases(8) = class'TEST_TextAPI' + testCases(9) = class'TEST_Parser' + testCases(10) = class'TEST_JSON' + testCases(11) = class'TEST_TextCache' + testCases(12) = class'TEST_User' + testCases(13) = class'TEST_Memory' + testCases(14) = class'TEST_DynamicArray' + testCases(15) = class'TEST_AssociativeArray' + testCases(16) = class'TEST_CollectionsMixed' + testCases(17) = class'TEST_Iterator' + testCases(18) = class'TEST_Command' + testCases(19) = class'TEST_CommandDataBuilder' + testCases(20) = class'TEST_LogMessage' } \ No newline at end of file