Browse Source

Refactor signals/slots

Refactor signal/slots system for more efficieny and better user
interface that allows to disconnect receiver like so:
`OnEvent(receiver).Disconnect()`.
pull/8/head
Anton Tarasenko 4 years ago
parent
commit
370f5efa39
  1. 235
      sources/Events/Signal.uc
  2. 53
      sources/Events/Slot.uc
  3. 64
      sources/Events/Tests/MockSignalSlotClient.uc
  4. 72
      sources/Events/Tests/TEST_SignalsSlots.uc

235
sources/Events/Signal.uc

@ -38,28 +38,53 @@
class Signal extends AcediaObject class Signal extends AcediaObject
abstract; abstract;
/**
* `Signal` essentially has to provide functionality for
* connecting/disconnecting slots and iterating through them. The main
* challenge is that slots can also be connected / disconnected during
* the signal emission. And in such cases we want:
* 1. To not propagate a signal to `Slot`s that were added during
* it's emission;
* 2. To not propagate a signal to any removed `Slot`, even if it was
* connected to the `Signal` in question when signal started emitting.
*
* We store connected `Slot`s in array, so to iterate we will simply use
* internal index variable `nextSlotIndex`. To account for removal of `Slot`s
* we will simply have to appropriately correct `nextSlotIndex` variable.
* To account for adding `Slot`s during signal emission we will first add them
* to a temporary queue `slotQueueToAdd` and only dump signals stored there
* into actual connected `Slot`s array before next iteration starts.
*/
// Class of the slot that can catch your `Signal` class // Class of the slot that can catch your `Signal` class
var public const class<Slot> relatedSlotClass; var public const class<Slot> relatedSlotClass;
// We want to always return a non-`none` slot to avoid "Access 'none'" // Set to `true` when we are in the process of removing connected `Slot`s.
// 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<Slot> 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 // Once `Slot` is deallocated, it notifies it's `Signal` (us) that it
// should be removed. // should be removed.
// But if it's deallocated because we are removing it we want to ignore // But if it's deallocated because we are removing it, then we want to
// their notification and this flag helps us do that. // ignore that notification and this flag helps us do that.
var private bool doingSelfCleaning; var private bool doingSelfCleaning;
// Used to provide iterating interface (`StartIterating()` / `GetNextSlot()`). // Used to provide iterating interface (`StartIterating()` / `GetNextSlot()`).
// Points at the next slot to return. // Points at the next slot to return.
var private int nextSlotIndex; var private int nextSlotIndex;
// These arrays could be defined as one array of `struct`s with four // This record describes slot-receiver pair to be added, along with it's
// elements. // life versions at the moment of adding a slot. Life versions help us verify
// that slot/receiver were not re-allocated at some point
// (thus becoming different objects).
struct SlotRecord
{
var Slot slotInstance;
var int slotLifeVersion;
var AcediaObject receiver;
var int receiverLifeVersion;
};
// Slots to be added before the next iteration (signal emission).
// We ensure that any added record has `slotInstance != none`.
var array<SlotRecord> slotQueueToAdd;
// These arrays could be defined as one array of `SlotRecord` structs.
// We use four different arrays instead for performance reasons. // We use four different arrays instead for performance reasons.
// They must have the same length at all times and elements with the // They must have the same length at all times and elements with the
// same index correspond to the same "record". // same index correspond to the same "record".
@ -92,7 +117,7 @@ public final function Emit(<PARAMETERS>)
/* TEMPLATE for handlers with returned values: /* TEMPLATE for handlers with returned values:
public final function int Emit(<PARAMETERS>) public final function <RETURN_TYPE> Emit(<PARAMETERS>)
{ {
local <RETURN_TYPE> newValue; local <RETURN_TYPE> newValue;
local Slot nextSlot; local Slot nextSlot;
@ -109,7 +134,8 @@ public final function int Emit(<PARAMETERS>)
nextSlot = GetNextSlot(); nextSlot = GetNextSlot();
} }
CleanEmptySlots(); CleanEmptySlots();
return value; // Return whatever you see fit after handling all the slots
return <END_RETURN_VALUE>;
} }
*/ */
@ -124,10 +150,21 @@ public final function <SLOT_CLASS> OnMyEvent(AcediaObject receiver)
protected function Finalizer() protected function Finalizer()
{ {
local int i;
doingSelfCleaning = true; doingSelfCleaning = true;
_.memory.FreeMany(registeredSlots); // Free queue for slot addition
doingSelfCleaning = false; for (i = 0; i < slotQueueToAdd.length; i += 1)
{
slotQueueToAdd[i].slotInstance
.FreeSelf(slotQueueToAdd[i].slotLifeVersion);
}
slotQueueToAdd.length = 0;
// Free actually connected slots
for (i = 0; i < registeredSlots.length; i += 1) {
registeredSlots[i].FreeSelf(slotLifeVersions[i]);
}
registeredSlots.length = 0; registeredSlots.length = 0;
doingSelfCleaning = false;
slotLifeVersions.length = 0; slotLifeVersions.length = 0;
slotReceivers.length = 0; slotReceivers.length = 0;
slotReceiversLifeVersions.length = 0; slotReceiversLifeVersions.length = 0;
@ -150,7 +187,7 @@ public final function Slot NewSlot(AcediaObject receiver)
{ {
local Slot newSlot; local Slot newSlot;
newSlot = Slot(_.memory.Allocate(relatedSlotClass)); newSlot = Slot(_.memory.Allocate(relatedSlotClass));
newSlot.Initialize(self); newSlot.Initialize(self, receiver);
AddSlot(newSlot, receiver); AddSlot(newSlot, receiver);
return newSlot; return newSlot;
} }
@ -158,21 +195,38 @@ public final function Slot NewSlot(AcediaObject receiver)
/** /**
* Disconnects all of the `receiver`'s `Slot`s from the caller `Signal`. * Disconnects all of the `receiver`'s `Slot`s from the caller `Signal`.
* *
* Meant to only be used by the `Slot`s `Disconnect()` method.
*
* @param receiver Object to disconnect from the caller `Signal`. * @param receiver Object to disconnect from the caller `Signal`.
* If `none` is passed, does nothing.
*/ */
public final function Disconnect(AcediaObject receiver) public final function Disconnect(AcediaObject receiver)
{ {
local int i; local int i;
if (receiver == none) {
return;
}
doingSelfCleaning = true; doingSelfCleaning = true;
while (i < slotReceivers.length) // Clean from the queue for addition
i = 0;
while (i < slotQueueToAdd.length)
{ {
if (slotReceivers[i] == none || slotReceivers[i] == receiver) if (slotQueueToAdd[i].receiver == receiver)
{ {
_.memory.Free(registeredSlots[i]); slotQueueToAdd[i].slotInstance
registeredSlots.Remove(i, 1); .FreeSelf(slotQueueToAdd[i].slotLifeVersion);
slotLifeVersions.Remove(i, 1); slotQueueToAdd.Remove(i, 1);
slotReceivers.Remove(i, 1); }
slotReceiversLifeVersions.Remove(i, 1); else {
i += 1;
}
}
// Clean from the active slots
i = 0;
while (i < slotReceivers.length)
{
if (slotReceivers[i] == receiver) {
RemoveSlotAtIndex(i);
} }
else { else {
i += 1; i += 1;
@ -182,56 +236,91 @@ public final function Disconnect(AcediaObject receiver)
} }
/** /**
* Adds new `Slot` `newSlot` with receiver `receiver` to the caller `Signal`. * Adds new `Slot` (`newSlot`) with receiver `receiver` to the caller `Signal`.
* *
* Does nothing if `newSlot` is already added to the caller `Signal`. * Does nothing if `newSlot` is already added to the caller `Signal`
* (even if it's added with a different receiver).
* *
* @param newSlot Slot to add. Must be initialize for 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. * @param receiver Receiver to which new `Slot` would be connected.
* Method connected to a `Slot` generated by this method must belong to * Method connected to a `Slot` generated by this method must belong to
* the `receiver`, otherwise behavior of `Signal`-`Slot` system is * the `receiver`, otherwise behavior of `Signal`-`Slot` system is
* undefined. * undefined. Must be a properly allocated `AcediaObject`.
* Must be a properly allocated `AcediaObject`.
*/ */
protected final function AddSlot(Slot newSlot, AcediaObject receiver) protected final function AddSlot(Slot newSlot, AcediaObject receiver)
{ {
local int i; local SlotRecord newRecord;
local int newSlotIndex; if (newSlot == none) {
if (newSlot == none) return; return;
if (newSlot.class != relatedSlotClass) return; }
if (!newSlot.IsOwnerSignal(self)) return; newRecord.slotInstance = newSlot;
if (receiver == none || !receiver.IsAllocated()) newRecord.slotLifeVersion = newSlot.GetLifeVersion();
newRecord.receiver = receiver;
if (receiver != none) {
newRecord.receiverLifeVersion = receiver.GetLifeVersion();
}
slotQueueToAdd[slotQueueToAdd.length] = newRecord;
}
// Attempts to add a `Slot` from a `SlotRecord` into array of currently
// connected `Slot`s.
// IMPORTANT: Must only be called right before a new iteration
// (signal emission) through the `Slot`s. Otherwise `Signal`'s behavior
// should be considered undefined.
private final function AddSlotRecord(SlotRecord record)
{
local int i;
local int newSlotIndex;
local Slot newSlot;
local AcediaObject receiver;
newSlot = record.slotInstance;
receiver = record.receiver;
if (newSlot.class != relatedSlotClass) return;
if (!newSlot.IsOwnerSignal(self)) return;
// Slot got outdated while waiting in queue
if (newSlot.GetLifeVersion() != record.slotLifeVersion) return;
// Receiver is outright invalid or got outdated
if ( receiver == none
|| !receiver.IsAllocated()
|| receiver.GetLifeVersion() != record.receiverLifeVersion)
{ {
failedSlots[failedSlots.length] = newSlot; doingSelfCleaning = true;
newSlot.FreeSelf();
doingSelfCleaning = false;
return; return;
} }
// Check if that slot is already added
for (i = 0; i < registeredSlots.length; i += 1) for (i = 0; i < registeredSlots.length; i += 1)
{ {
if (registeredSlots[i] != newSlot) { if (registeredSlots[i] != newSlot) {
continue; continue;
} }
if (slotLifeVersions[i] != newSlot.GetLifeVersion()) // If we have the same instance recorded, but...
// 1. it was reallocated: update it's records;
// 2. it was not reallocated: leave the records intact.
// Neither would case issues with iterating along `Slot`s if this
// method is only called right before new iteration.
if (slotLifeVersions[i] != record.slotLifeVersion)
{ {
slotLifeVersions[i] = newSlot.GetLifeVersion(); slotLifeVersions[i] = record.slotLifeVersion;
slotReceivers[i] = receiver; slotReceivers[i] = receiver;
if (receiver != none) { if (receiver != none) {
slotReceiversLifeVersions[i] = receiver.GetLifeVersion(); slotReceiversLifeVersions[i] = record.receiverLifeVersion;
} }
} }
return; return;
} }
newSlotIndex = registeredSlots.length; newSlotIndex = registeredSlots.length;
registeredSlots[newSlotIndex] = newSlot; registeredSlots[newSlotIndex] = newSlot;
slotLifeVersions[newSlotIndex] = newSlot.GetLifeVersion(); slotLifeVersions[newSlotIndex] = record.slotLifeVersion;
slotReceivers[newSlotIndex] = receiver; slotReceivers[newSlotIndex] = receiver;
slotReceiversLifeVersions[newSlotIndex] = receiver.GetLifeVersion(); slotReceiversLifeVersions[newSlotIndex] = record.receiverLifeVersion;
} }
/** /**
* Removes given `slotToRemove` if it was connected to the caller `Signal`. * Removes given `slotToRemove` if it was connected to the caller `Signal`.
* *
* Does not deallocate `slotToRemove`.
*
* Cannot fail. * Cannot fail.
* *
* @param slotToRemove Slot to be removed. * @param slotToRemove Slot to be removed.
@ -242,14 +331,24 @@ public final function RemoveSlot(Slot slotToRemove)
if (slotToRemove == none) return; if (slotToRemove == none) return;
if (doingSelfCleaning) return; if (doingSelfCleaning) return;
// Remove from queue for addition
while (i < slotQueueToAdd.length)
{
if (slotQueueToAdd[i].slotInstance == slotToRemove)
{
slotToRemove.FreeSelf(slotQueueToAdd[i].slotLifeVersion);
slotQueueToAdd.Remove(i, 1);
}
else {
i += 1;
}
}
// Remove from active slots
for (i = 0; i < registeredSlots.length; i += 1) for (i = 0; i < registeredSlots.length; i += 1)
{ {
if (registeredSlots[i] == slotToRemove) if (registeredSlots[i] == slotToRemove)
{ {
registeredSlots.Remove(i, 1); RemoveSlotAtIndex(i);
slotLifeVersions.Remove(i, 1);
slotReceivers.Remove(i, 1);
slotReceiversLifeVersions.Remove(i, 1);
return; return;
} }
} }
@ -265,6 +364,11 @@ public final function RemoveSlot(Slot slotToRemove)
*/ */
protected final function StartIterating() protected final function StartIterating()
{ {
local int i;
for (i = 0; i < slotQueueToAdd.length; i += 1) {
AddSlotRecord(slotQueueToAdd[i]);
}
slotQueueToAdd.length = 0;
nextSlotIndex = 0; nextSlotIndex = 0;
} }
@ -298,15 +402,11 @@ protected final function Slot GetNextSlot()
if (isNextSlotValid) if (isNextSlotValid)
{ {
nextSlotIndex += 1; nextSlotIndex += 1;
doingSelfCleaning = false;
return nextSlot; return nextSlot;
} }
else else {
{ RemoveSlotAtIndex(nextSlotIndex);
registeredSlots.Remove(nextSlotIndex, 1);
slotLifeVersions.Remove(nextSlotIndex, 1);
slotReceivers.Remove(nextSlotIndex, 1);
slotReceiversLifeVersions.Remove(nextSlotIndex, 1);
_.memory.Free(nextSlot);
} }
} }
doingSelfCleaning = false; doingSelfCleaning = false;
@ -315,24 +415,16 @@ protected final function Slot GetNextSlot()
/** /**
* In case it's detected that some of the slots do not actually have any * In case it's detected that some of the slots do not actually have any
* handler setup - this method will clean them up. * delegate set - this method will clean them up.
*/ */
protected final function CleanEmptySlots() protected final function CleanEmptySlots()
{ {
local int index; local int index;
_.memory.FreeMany(failedSlots);
failedSlots.length = 0;
doingSelfCleaning = true; doingSelfCleaning = true;
while (index < registeredSlots.length) while (index < registeredSlots.length)
{ {
if (registeredSlots[index].IsEmpty()) if (registeredSlots[index].IsEmpty()) {
{ RemoveSlotAtIndex(index);
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 { else {
index += 1; index += 1;
@ -341,6 +433,21 @@ protected final function CleanEmptySlots()
doingSelfCleaning = false; doingSelfCleaning = false;
} }
// Removes `Slot` at a given `index`.
// Assumes that passed index is within boundaries.
private final function RemoveSlotAtIndex(int index)
{
registeredSlots[index].FreeSelf(slotLifeVersions[index]);
registeredSlots.Remove(index, 1);
slotLifeVersions.Remove(index, 1);
slotReceivers.Remove(index, 1);
slotReceiversLifeVersions.Remove(index, 1);
// Alter iteration index `nextSlotIndex` to account for this `Slot` removal
if (nextSlotIndex > index) {
nextSlotIndex -= 1;
}
}
defaultproperties defaultproperties
{ {
relatedSlotClass = class'Slot' relatedSlotClass = class'Slot'

53
sources/Events/Slot.uc

@ -33,8 +33,10 @@
class Slot extends AcediaObject class Slot extends AcediaObject
abstract; abstract;
var private bool dummyMethodCalled; var private bool dummyMethodCalled;
var private Signal ownerSignal; var private AcediaObject myReceiver;
var private int myReceiverLifeVersion;
var private Signal mySignal;
/* TEMPLATE for handlers without returned values: /* TEMPLATE for handlers without returned values:
delegate connect(<PARAMETERS>) delegate connect(<PARAMETERS>)
@ -78,10 +80,11 @@ protected function Finalizer()
protected function Finalizer() protected function Finalizer()
{ {
dummyMethodCalled = false; dummyMethodCalled = false;
if (ownerSignal != none) { if (mySignal != none) {
ownerSignal.RemoveSlot(self); mySignal.RemoveSlot(self);
} }
ownerSignal = none; mySignal = none;
myReceiver = none;
} }
/** /**
@ -93,8 +96,8 @@ protected function Finalizer()
protected final function DummyCall() protected final function DummyCall()
{ {
dummyMethodCalled = true; dummyMethodCalled = true;
// We do not want to call `ownerSignal.RemoveSlot(self)` here, since // We do not want to call `mySignal.RemoveSlot(self)` here, since
// `ownerSignal` is likely in process of iterating through it's `Slot`s // `mySignal` is likely in process of iterating through it's `Slot`s
// and removing (or adding) `Slot`s from it can mess up that process. // and removing (or adding) `Slot`s from it can mess up that process.
} }
@ -104,21 +107,29 @@ protected final function DummyCall()
* Can only be done once for every `Slot`. * Can only be done once for every `Slot`.
* *
* @param newOwnerSignal `Signal` we want to receive emitted signals from. * @param newOwnerSignal `Signal` we want to receive emitted signals from.
* @param receiver Receiver object that caused creation of this `Slot`.
* @return `true` if initialization was successful and `false` otherwise * @return `true` if initialization was successful and `false` otherwise
* (if `newOwnerSignal` is invalid or caller `Slot` was * (if `newOwnerSignal` is invalid or caller `Slot` was
* already initialized). * already initialized).
*/ */
public final function bool Initialize(Signal newOwnerSignal) public final function bool Initialize(
Signal newOwnerSignal,
AcediaObject receiver)
{ {
if (ownerSignal != none) { if (mySignal != none) {
return false; return false;
} }
if (receiver != none && receiver.IsAllocated())
{
myReceiver = receiver;
myReceiverLifeVersion = receiver.GetLifeVersion();
}
if (newOwnerSignal == none || !newOwnerSignal.IsAllocated()) if (newOwnerSignal == none || !newOwnerSignal.IsAllocated())
{ {
FreeSelf(); FreeSelf();
return false; return false;
} }
ownerSignal = newOwnerSignal; mySignal = newOwnerSignal;
return true; return true;
} }
@ -131,7 +142,27 @@ public final function bool Initialize(Signal newOwnerSignal)
*/ */
public final function bool IsOwnerSignal(Signal testSignal) public final function bool IsOwnerSignal(Signal testSignal)
{ {
return (ownerSignal == testSignal); return (mySignal == testSignal);
}
/**
* Disconnects all the `Slot`s that belong to receiver of the caller `Slot`
* from our owner signal `mySignal`.
*
* Method's name does not reflect what it does well, however this is done
* deliberately to provide an overall more intuitive user interface to
* signals and slots that allows for receiver to disconnect from `Signal` in
* the similar way to how it connects:
* `eventSource.OnSomeEvent(self).connect = handler`
* `eventSource.OnSomeEvent(self).Disconnect()`
*/
public final function Disconnect()
{
if (mySignal == none) return;
if (myReceiver == none) return;
if (myReceiver.GetLifeVersion() != myReceiverLifeVersion) return;
mySignal.Disconnect(myReceiver);
} }
/** /**

64
sources/Events/Tests/MockSignalSlotClient.uc

@ -20,22 +20,57 @@
*/ */
class MockSignalSlotClient extends AcediaObject; class MockSignalSlotClient extends AcediaObject;
var private int value; // Remember `Signal` and `Slot` for testing purposes
var private MockSignal usedSignal;
var private MockSlot usedSlot;
var private int value;
public final function SetValue(int newValue) public final function SetValue(int newValue)
{ {
value = newValue; value = newValue;
} }
public final function DisconnectMe(MockSignal signal)
{
if (signal != none) {
signal.NewSlot(self).Disconnect();
}
}
// Return `SMockSlot` for testing purposes // Return `SMockSlot` for testing purposes
public final function MockSlot AddToSignal(MockSignal signal) public final function MockSlot AddToSignal(MockSignal signal)
{ {
local MockSlot slot; local MockSlot slot;
if (signal == none) {
return none;
}
slot = MockSlot(signal.NewSlot(self)); slot = MockSlot(signal.NewSlot(self));
slot.connect = Handler; slot.connect = Handler;
return slot; return slot;
} }
public final function MockSlot AddToSignal_AddNewSlot(MockSignal signal)
{
local MockSlot slot;
if (signal == none) {
return none;
}
usedSignal = signal;
slot = MockSlot(signal.NewSlot(self));
slot.connect = Handler_AddNewSlot;
return slot;
}
public final function MockSlot AddToSignal_DestroySlot(MockSignal signal)
{
if (signal == none) {
return none;
}
usedSlot = MockSlot(signal.NewSlot(self));
usedSlot.connect = Handler_DestroySlot;
return usedSlot;
}
private final function int Handler(int inputValue, optional bool doSubtract) private final function int Handler(int inputValue, optional bool doSubtract)
{ {
if (doSubtract) { if (doSubtract) {
@ -44,6 +79,33 @@ private final function int Handler(int inputValue, optional bool doSubtract)
return inputValue + value; return inputValue + value;
} }
private final function int Handler_AddNewSlot(
int inputValue,
optional bool doSubtract)
{
local MockSignalSlotClient newClient;
if (usedSignal == none) {
return inputValue;
}
newClient = MockSignalSlotClient(
_.memory.Allocate(class'MockSignalSlotClient'));
newClient.SetValue(value);
newClient.AddToSignal(usedSignal);
usedSignal = none;
return inputValue;
}
private final function int Handler_DestroySlot(
int inputValue,
optional bool doSubtract)
{
if (usedSlot != none) {
usedSlot.FreeSelf();
}
usedSlot = none;
return inputValue;
}
defaultproperties defaultproperties
{ {
} }

72
sources/Events/Tests/TEST_SignalsSlots.uc

@ -26,11 +26,16 @@ protected static function TESTS()
@ "a signal."); @ "a signal.");
Test_Connecting(); Test_Connecting();
Test_Disconnecting(); Test_Disconnecting();
Test_DisconnectingThroughSlot();
Context("Testing how signals and slots system handles deallocations and" Context("Testing how signals and slots system handles deallocations and"
@ "unexpected changes to managed objects."); @ "unexpected changes to managed objects.");
Test_DeallocSlots(); Test_DeallocSlots();
Test_EmptySlots(); Test_EmptySlots();
Test_DeallocReceivers(); Test_DeallocReceivers();
Context("Testing how signals and slots system handles connecting and"
@ "disconnections of slots while a signal is being emitted.");
Test_ConnectSlotsDuringEmittion();
Test_DisconnectSlotsDuringEmittion();
} }
protected static function Test_Connecting() protected static function Test_Connecting()
@ -95,6 +100,25 @@ protected static function Test_Disconnecting()
__().memory.FreeMany(objects); __().memory.FreeMany(objects);
} }
protected static function Test_DisconnectingThroughSlot()
{
local MockSignal signal;
local MockSlot slot1, slot2, slot3;
local MockSignalSlotClient client;
Issue("`Disconnect()` defined for `Slot()` does not properly disconnect all"
@ "of it's receiver's slots.");
signal = MockSignal(__().memory.Allocate(class'MockSignal'));
client = MockSignalSlotClient(
__().memory.Allocate(class'MockSignalSlotClient'));
slot1 = client.AddToSignal_AddNewSlot(signal);
slot2 = client.AddToSignal_DestroySlot(signal);
slot3 = client.AddToSignal(signal);
client.DisconnectMe(signal);
TEST_ExpectFalse(slot1.IsAllocated());
TEST_ExpectFalse(slot2.IsAllocated());
TEST_ExpectFalse(slot3.IsAllocated());
}
protected static function Test_DeallocSlots() protected static function Test_DeallocSlots()
{ {
local int i; local int i;
@ -202,11 +226,57 @@ protected static function Test_DeallocReceivers()
} }
} }
Issue("Slots with deallocated receivers are not deallocated."); Issue("Slots with deallocated receivers are not deallocated.");
TEST_ExpectFalse(true); TEST_ExpectFalse(slotsAreNotDeallocated);
__().memory.Free(signal); __().memory.Free(signal);
__().memory.FreeMany(objects); __().memory.FreeMany(objects);
} }
protected static function Test_ConnectSlotsDuringEmittion()
{
local MockSignal signal;
local MockSignalSlotClient clientWithNewConnection, regularClient;
Issue("Slots added during signal emission also receive that signal.");
signal = MockSignal(__().memory.Allocate(class'MockSignal'));
clientWithNewConnection = MockSignalSlotClient(
__().memory.Allocate(class'MockSignalSlotClient'));
regularClient = MockSignalSlotClient(
__().memory.Allocate(class'MockSignalSlotClient'));
clientWithNewConnection.AddToSignal_AddNewSlot(signal);
regularClient.AddToSignal(signal);
clientWithNewConnection.SetValue(27);
regularClient.SetValue(23);
// Ideally `clientWithNewConnection` only adds a new slot to which
// an object with the same value as `clientWithNewConnection` is connected.
// That newly added object should not affect hat same signal emission.
// This test checks that it actually does not, which would mean that
// only `regularClient` alters resulting value and sets it to `23`.
TEST_ExpectTrue(signal.Emit(0) == 23);
}
protected static function Test_DisconnectSlotsDuringEmittion()
{
local MockSignal signal;
local MockSignalSlotClient clientThatDestroys, regularClient;
Issue("Some slots do not receive a signal if one of them was removed during"
@ "that signal's emission.");
signal = MockSignal(__().memory.Allocate(class'MockSignal'));
clientThatDestroys = MockSignalSlotClient(
__().memory.Allocate(class'MockSignalSlotClient'));
regularClient = MockSignalSlotClient(
__().memory.Allocate(class'MockSignalSlotClient'));
clientThatDestroys.AddToSignal_DestroySlot(signal);
regularClient.AddToSignal(signal);
regularClient.SetValue(23);
// Ideally `clientThatDestroys` only removes itself from a `signal`,
// without affecting whether other `Slot`s receive a signal. However,
// in one of the previous implementations that prevented the next `Slot`
// from receiving signal emission.
// This test checks that this does not actually happen, which would
// mean that `regularClient` still alters resulting value and sets it
// to `23`.
TEST_ExpectTrue(signal.Emit(0) == 23);
}
defaultproperties defaultproperties
{ {
caseGroup = "Events" caseGroup = "Events"

Loading…
Cancel
Save