From bbd341969a655f2fe55548c777bc0fe51edd4dd9 Mon Sep 17 00:00:00 2001 From: Jeff Tung <100387939+jtung-apple@users.noreply.github.com> Date: Wed, 12 Jun 2024 18:38:11 -0700 Subject: [PATCH] [Darwin] MTRDevice should support multiple delegates (#32491) * [Darwin] MTRDevice should support multiple delegates * Better naming, comments, and removal of unused ivar * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Update src/darwin/Framework/CHIP/MTRDevice.h Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Boris Zbarsky * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Addressed review comments * Address logging review comment * Fix tests caused by new nullability change * Fix unit test --------- Co-authored-by: Justin Wood Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> Co-authored-by: Boris Zbarsky --- src/darwin/Framework/CHIP/MTRDevice.h | 33 +- src/darwin/Framework/CHIP/MTRDevice.mm | 605 +++++++++++++----- .../MTRDeviceStorageBehaviorConfiguration.h | 4 +- .../Framework/CHIPTests/MTRDeviceTests.m | 324 ++++++++++ .../TestHelpers/MTRTestDeclarations.h | 1 + 5 files changed, 809 insertions(+), 158 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index 08aae192468337..c605a8b70753a1 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -114,7 +114,36 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) * * The delegate will be called on the provided queue, for attribute reports, event reports, and device state changes. */ -- (void)setDelegate:(id)delegate queue:(dispatch_queue_t)queue; +- (void)setDelegate:(id)delegate queue:(dispatch_queue_t)queue MTR_NEWLY_DEPRECATED("Please use addDelegate:queue:interestedPaths:"); + +/** + * Adds a delegate to receive asynchronous callbacks about the device. + * + * The delegate will be called on the provided queue, for attribute reports, event reports, and device state changes. + * + * MTRDevice holds a weak reference to the delegate object. + */ +- (void)addDelegate:(id)delegate queue:(dispatch_queue_t)queue MTR_NEWLY_AVAILABLE; + +/** + * Adds a delegate to receive asynchronous callbacks about the device, and limit attribute and/or event reports to a specific set of paths. + * + * interestedPathsForAttributes may contain either MTRClusterPath or MTRAttributePath to specify interested clusters and attributes, or NSNumber for endpoints. + * + * interestedPathsForAttributes may contain either MTRClusterPath or MTREventPath to specify interested clusters and events, or NSNumber for endpoints. + * + * For both interested paths arguments, if nil is specified, then no filter will be applied. + * + * Calling addDelegate: again with the same delegate object will update the interested paths for attributes and events for this delegate. + * + * MTRDevice holds a weak reference to the delegate object. + */ +- (void)addDelegate:(id)delegate queue:(dispatch_queue_t)queue interestedPathsForAttributes:(NSArray * _Nullable)interestedPathsForAttributes interestedPathsForEvents:(NSArray * _Nullable)interestedPathsForEvents MTR_NEWLY_AVAILABLE; + +/** + * Removes the delegate from receiving callbacks about the device. + */ +- (void)removeDelegate:(id)delegate MTR_NEWLY_AVAILABLE; /** * Read attribute in a designated attribute path. If there is no value available @@ -389,7 +418,7 @@ MTR_EXTERN NSString * const MTRDataVersionKey MTR_AVAILABLE(ios(17.6), macos(14. * * The data-value dictionary also contains this key: * - * MTRDataVersionKey : NSNumber-wrapped uin32_t. Monotonically increaseing data version for the cluster. + * MTRDataVersionKey : NSNumber-wrapped uin32_t. */ - (void)device:(MTRDevice *)device receivedAttributeReport:(NSArray *> *)attributeReport; diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 9a9b2eb340d107..54fa8ece393156 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -62,35 +62,80 @@ // Consider moving utility classes to their own file #pragma mark - Utility Classes -// This class is for storing weak references in a container -@interface MTRWeakReference : NSObject -+ (instancetype)weakReferenceWithObject:(ObjectType)object; -- (instancetype)initWithObject:(ObjectType)object; -- (ObjectType)strongObject; // returns strong object or NULL -@end -@interface MTRWeakReference () { +// container of MTRDevice delegate weak reference, its queue, and its interested paths for attribute reports +MTR_DIRECT_MEMBERS +@interface MTRDeviceDelegateInfo : NSObject { @private - __weak id _object; + void * _delegatePointerValue; + __weak id _delegate; + dispatch_queue_t _queue; + NSArray * _Nullable _interestedPathsForAttributes; + NSArray * _Nullable _interestedPathsForEvents; } + +// Array of interested cluster paths, attribute paths, or endpointID, for attribute report filtering. +@property (readonly, nullable) NSArray * interestedPathsForAttributes; + +// Array of interested cluster paths, attribute paths, or endpointID, for event report filtering. +@property (readonly, nullable) NSArray * interestedPathsForEvents; + +// Expose delegate +@property (readonly) id delegate; + +// Pointer value for logging purpose only +@property (readonly) void * delegatePointerValue; + +- (instancetype)initWithDelegate:(id)delegate queue:(dispatch_queue_t)queue interestedPathsForAttributes:(NSArray * _Nullable)interestedPathsForAttributes interestedPathsForEvents:(NSArray * _Nullable)interestedPathsForEvents; + +// Returns YES if delegate and queue are both non-null, and the block is scheduled to run. +- (BOOL)callDelegateWithBlock:(void (^)(id))block; + +#ifdef DEBUG +// Only used for unit test purposes - normal delegate should not expect or handle being called back synchronously. +- (BOOL)callDelegateSynchronouslyWithBlock:(void (^)(id))block; +#endif @end -@implementation MTRWeakReference -- (instancetype)initWithObject:(id)object +@implementation MTRDeviceDelegateInfo +- (instancetype)initWithDelegate:(id)delegate queue:(dispatch_queue_t)queue interestedPathsForAttributes:(NSArray * _Nullable)interestedPathsForAttributes interestedPathsForEvents:(NSArray * _Nullable)interestedPathsForEvents { if (self = [super init]) { - _object = object; + _delegate = delegate; + _delegatePointerValue = (__bridge void *) delegate; + _queue = queue; + _interestedPathsForAttributes = [interestedPathsForAttributes copy]; + _interestedPathsForEvents = [interestedPathsForEvents copy]; } return self; } -+ (instancetype)weakReferenceWithObject:(id)object + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", self, _delegatePointerValue, static_cast(_interestedPathsForAttributes.count), static_cast(_interestedPathsForEvents.count)]; +} + +- (BOOL)callDelegateWithBlock:(void (^)(id))block { - return [[self alloc] initWithObject:object]; + id strongDelegate = _delegate; + VerifyOrReturnValue(strongDelegate, NO); + dispatch_async(_queue, ^{ + block(strongDelegate); + }); + return YES; } -- (id)strongObject + +#ifdef DEBUG +- (BOOL)callDelegateSynchronouslyWithBlock:(void (^)(id))block { - return _object; + id strongDelegate = _delegate; + VerifyOrReturnValue(strongDelegate, NO); + + block(strongDelegate); + + return YES; } +#endif @end NSNumber * MTRClampedNumber(NSNumber * aNumber, NSNumber * min, NSNumber * max) @@ -321,8 +366,6 @@ @interface MTRDevice () // and protects device calls to setUTCTime and setDSTOffset @property (nonatomic, readonly) os_unfair_lock timeSyncLock; @property (nonatomic) chip::FabricIndex fabricIndex; -@property (nonatomic) MTRWeakReference> * weakDelegate; -@property (nonatomic) dispatch_queue_t delegateQueue; @property (nonatomic) NSMutableArray *> * unreportedEvents; @property (nonatomic) BOOL receivingReport; @property (nonatomic) BOOL receivingPrimingReport; @@ -445,6 +488,8 @@ @implementation MTRDevice { // System time change observer reference id _systemTimeChangeObserverToken; + + NSMutableSet * _delegates; } - (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller @@ -469,7 +514,8 @@ - (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceControlle _clusterDataToPersist = nil; _persistedClusters = [NSMutableSet set]; - // If there is a data store, make sure we have an observer to + // If there is a data store, make sure we have an observer to monitor system clock changes, so + // NSDate-based write coalescing could be reset and not get into a bad state. if (_persistedClusterData) { mtr_weakify(self); _systemTimeChangeObserverToken = [[NSNotificationCenter defaultCenter] addObserverForName:NSSystemClockDidChangeNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull notification) { @@ -479,6 +525,8 @@ - (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceControlle }]; } + _delegates = [NSMutableSet set]; + MTR_LOG_DEBUG("%@ init with hex nodeID 0x%016llX", self, _nodeID.unsignedLongLongValue); } return self; @@ -560,12 +608,12 @@ - (void)_setTimeOnDevice - (void)_scheduleNextUpdate:(UInt64)nextUpdateInSeconds { - MTRWeakReference * weakSelf = [MTRWeakReference weakReferenceWithObject:self]; + mtr_weakify(self); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (nextUpdateInSeconds * NSEC_PER_SEC)), self.queue, ^{ MTR_LOG_DEBUG("%@ Timer expired, start Device Time Update", self); - MTRDevice * strongSelf = weakSelf.strongObject; - if (strongSelf) { - [strongSelf _performScheduledTimeUpdate]; + mtr_strongify(self); + if (self) { + [self _performScheduledTimeUpdate]; } else { MTR_LOG_DEBUG("%@ MTRDevice no longer valid. No Timer Scheduled will be scheduled for a Device Time Update.", self); return; @@ -738,25 +786,64 @@ - (BOOL)_subscriptionsAllowed - (void)setDelegate:(id)delegate queue:(dispatch_queue_t)queue { MTR_LOG("%@ setDelegate %@", self, delegate); + [self _addDelegate:delegate queue:queue interestedPathsForAttributes:nil interestedPathsForEvents:nil]; +} +- (void)addDelegate:(id)delegate queue:(dispatch_queue_t)queue +{ + MTR_LOG("%@ addDelegate %@", self, delegate); + [self _addDelegate:delegate queue:queue interestedPathsForAttributes:nil interestedPathsForEvents:nil]; +} + +- (void)addDelegate:(id)delegate queue:(dispatch_queue_t)queue interestedPathsForAttributes:(NSArray * _Nullable)interestedPathsForAttributes interestedPathsForEvents:(NSArray * _Nullable)interestedPathsForEvents MTR_NEWLY_AVAILABLE; +{ + MTR_LOG("%@ addDelegate %@ with interested attribute paths %@ event paths %@", self, delegate, interestedPathsForAttributes, interestedPathsForEvents); + [self _addDelegate:delegate queue:queue interestedPathsForAttributes:interestedPathsForAttributes interestedPathsForEvents:interestedPathsForEvents]; +} + +- (void)_addDelegate:(id)delegate queue:(dispatch_queue_t)queue interestedPathsForAttributes:(NSArray * _Nullable)interestedPathsForAttributes interestedPathsForEvents:(NSArray * _Nullable)interestedPathsForEvents +{ std::lock_guard lock(_lock); - BOOL setUpSubscription = [self _subscriptionsAllowed]; + // Replace delegate info with the same delegate object, and opportunistically remove defunct delegate references + NSMutableSet * delegatesToRemove = [NSMutableSet set]; + for (MTRDeviceDelegateInfo * delegateInfo in _delegates) { + id strongDelegate = delegateInfo.delegate; + if (!strongDelegate) { + [delegatesToRemove addObject:delegateInfo]; + MTR_LOG("%@ removing delegate info for nil delegate %p", self, delegateInfo.delegatePointerValue); + } else if (strongDelegate == delegate) { + [delegatesToRemove addObject:delegateInfo]; + MTR_LOG("%@ replacing delegate info for %p", self, delegate); + } + } + if (delegatesToRemove.count) { + NSUInteger oldDelegatesCount = _delegates.count; + [_delegates minusSet:delegatesToRemove]; + MTR_LOG("%@ addDelegate: removed %lu", self, static_cast(_delegates.count - oldDelegatesCount)); + } + + MTRDeviceDelegateInfo * newDelegateInfo = [[MTRDeviceDelegateInfo alloc] initWithDelegate:delegate queue:queue interestedPathsForAttributes:interestedPathsForAttributes interestedPathsForEvents:interestedPathsForEvents]; + [_delegates addObject:newDelegateInfo]; + MTR_LOG("%@ added delegate info %@", self, newDelegateInfo); + + __block BOOL shouldSetUpSubscription = [self _subscriptionsAllowed]; // For unit testing only. If this ever changes to not being for unit testing purposes, // we would need to move the code outside of where we acquire the lock above. #ifdef DEBUG - id testDelegate = delegate; - if ([testDelegate respondsToSelector:@selector(unitTestShouldSetUpSubscriptionForDevice:)]) { - setUpSubscription = [testDelegate unitTestShouldSetUpSubscriptionForDevice:self]; - } + [self _callFirstDelegateSynchronouslyWithBlock:^(id testDelegate) { + if ([testDelegate respondsToSelector:@selector(unitTestShouldSetUpSubscriptionForDevice:)]) { + shouldSetUpSubscription = [testDelegate unitTestShouldSetUpSubscriptionForDevice:self]; + } + }]; #endif - _weakDelegate = [MTRWeakReference weakReferenceWithObject:delegate]; - _delegateQueue = queue; - - if (setUpSubscription) { - _initialSubscribeStart = [NSDate now]; + if (shouldSetUpSubscription) { + // Record the time of first addDelegate call that triggers initial subscribe, and do not reset this value on subsequent addDelegate calls + if (!_initialSubscribeStart) { + _initialSubscribeStart = [NSDate now]; + } if ([self _deviceUsesThread]) { [self _scheduleSubscriptionPoolWork:^{ std::lock_guard lock(self->_lock); @@ -768,6 +855,27 @@ - (void)setDelegate:(id)delegate queue:(dispatch_queue_t)queu } } +- (void)removeDelegate:(id)delegate +{ + MTR_LOG("%@ removeDelegate %@", self, delegate); + + std::lock_guard lock(_lock); + + NSMutableSet * delegatesToRemove = [NSMutableSet set]; + [self _iterateDelegatesWithBlock:^(MTRDeviceDelegateInfo * delegateInfo) { + id strongDelegate = delegateInfo.delegate; + if (strongDelegate == delegate) { + [delegatesToRemove addObject:delegateInfo]; + MTR_LOG("%@ removing delegate info %@ for %p", self, delegateInfo, delegate); + } + }]; + if (delegatesToRemove.count) { + NSUInteger oldDelegatesCount = _delegates.count; + [_delegates minusSet:delegatesToRemove]; + MTR_LOG("%@ removeDelegate: removed %lu", self, static_cast(_delegates.count - oldDelegatesCount)); + } +} + - (void)invalidate { MTR_LOG("%@ invalidate", self); @@ -782,7 +890,7 @@ - (void)invalidate _state = MTRDeviceStateUnknown; - _weakDelegate = nil; + [_delegates removeAllObjects]; // Make sure we don't try to resubscribe if we have a pending resubscribe // attempt, since we now have no delegate. @@ -862,20 +970,25 @@ - (void)_triggerResubscribeWithReason:(NSString *)reason nodeLikelyReachable:(BO - (BOOL)_subscriptionAbleToReport { std::lock_guard lock(_lock); - id delegate = _weakDelegate.strongObject; - if (delegate == nil) { + if (![self _delegateExists]) { // No delegate definitely means no subscription. return NO; } // For unit testing only, matching logic in setDelegate #ifdef DEBUG - id testDelegate = delegate; - if ([testDelegate respondsToSelector:@selector(unitTestShouldSetUpSubscriptionForDevice:)]) { - if (![testDelegate unitTestShouldSetUpSubscriptionForDevice:self]) { - return NO; + __block BOOL useTestDelegateOverride = NO; + __block BOOL testDelegateShouldSetUpSubscriptionForDevice = NO; + [self _callFirstDelegateSynchronouslyWithBlock:^(id testDelegate) { + if ([testDelegate respondsToSelector:@selector(unitTestShouldSetUpSubscriptionForDevice:)]) { + useTestDelegateOverride = YES; + testDelegateShouldSetUpSubscriptionForDevice = [testDelegate unitTestShouldSetUpSubscriptionForDevice:self]; } + }]; + if (useTestDelegateOverride && !testDelegateShouldSetUpSubscriptionForDevice) { + return NO; } + #endif // Subscriptions are not able to report if they are not allowed. @@ -922,23 +1035,83 @@ - (void)_readThroughSkipped errorHandler:nil]; } -- (BOOL)_callDelegateWithBlock:(void (^)(id))block +- (BOOL)_delegateExists { os_unfair_lock_assert_owner(&self->_lock); - id delegate = _weakDelegate.strongObject; - if (delegate) { - dispatch_async(_delegateQueue, ^{ - block(delegate); - }); - return YES; + return [self _iterateDelegatesWithBlock:nil]; +} + +// Returns YES if any non-null delegates were found +- (BOOL)_iterateDelegatesWithBlock:(void(NS_NOESCAPE ^)(MTRDeviceDelegateInfo * delegateInfo)_Nullable)block +{ + os_unfair_lock_assert_owner(&self->_lock); + + if (!_delegates.count) { + MTR_LOG_DEBUG("%@ no delegates to iterate", self); + return NO; + } + + // Opportunistically remove defunct delegate references on every iteration + NSMutableSet * delegatesToRemove = nil; + for (MTRDeviceDelegateInfo * delegateInfo in _delegates) { + id strongDelegate = delegateInfo.delegate; + if (strongDelegate) { + if (block) { + @autoreleasepool { + block(delegateInfo); + } + } + (void) strongDelegate; // ensure it stays alive + } else { + if (!delegatesToRemove) { + delegatesToRemove = [NSMutableSet set]; + } + [delegatesToRemove addObject:delegateInfo]; + } + } + + if (delegatesToRemove.count) { + [_delegates minusSet:delegatesToRemove]; + MTR_LOG("%@ _iterateDelegatesWithBlock: removed %lu remaining %lu", self, static_cast(delegatesToRemove.count), (unsigned long) static_cast(_delegates.count)); + } + + return (_delegates.count > 0); +} + +- (BOOL)_callDelegatesWithBlock:(void (^)(id delegate))block +{ + os_unfair_lock_assert_owner(&self->_lock); + + __block NSUInteger delegatesCalled = 0; + [self _iterateDelegatesWithBlock:^(MTRDeviceDelegateInfo * delegateInfo) { + if ([delegateInfo callDelegateWithBlock:block]) { + delegatesCalled++; + } + }]; + + return (delegatesCalled > 0); +} + +#ifdef DEBUG +// Only used for unit test purposes - normal delegate should not expect or handle being called back synchronously +// Returns YES if a delegate is called +- (void)_callFirstDelegateSynchronouslyWithBlock:(void (^)(id delegate))block +{ + os_unfair_lock_assert_owner(&self->_lock); + + for (MTRDeviceDelegateInfo * delegateInfo in _delegates) { + if ([delegateInfo callDelegateSynchronouslyWithBlock:block]) { + MTR_LOG("%@ _callFirstDelegateSynchronouslyWithBlock: successfully called %@", self, delegateInfo); + return; + } } - return NO; } +#endif - (void)_callDelegateDeviceCachePrimed { os_unfair_lock_assert_owner(&self->_lock); - [self _callDelegateWithBlock:^(id delegate) { + [self _callDelegatesWithBlock:^(id delegate) { if ([delegate respondsToSelector:@selector(deviceCachePrimed:)]) { [delegate deviceCachePrimed:self]; } @@ -961,12 +1134,9 @@ - (void)_changeState:(MTRDeviceState)state MTR_LOG( "%@ reachability state change %lu => %lu", self, static_cast(lastState), static_cast(state)); } - id delegate = _weakDelegate.strongObject; - if (delegate) { - dispatch_async(_delegateQueue, ^{ - [delegate device:self stateChanged:state]; - }); - } + [self _callDelegatesWithBlock:^(id delegate) { + [delegate device:self stateChanged:state]; + }]; } else { MTR_LOG( "%@ Not reporting reachability state change, since no change in state %lu => %lu", self, static_cast(lastState), static_cast(state)); @@ -983,12 +1153,11 @@ - (void)_changeInternalState:(MTRInternalDeviceState)state /* BEGIN DRAGONS: This is a huge hack for a specific use case, do not rename, remove or modify behavior here */ // TODO: This should only be called for thread devices - id delegate = _weakDelegate.strongObject; - if ([delegate respondsToSelector:@selector(_deviceInternalStateChanged:)]) { - dispatch_async(_delegateQueue, ^{ - [(id) delegate _deviceInternalStateChanged:self]; - }); - } + [self _callDelegatesWithBlock:^(id delegate) { + if ([delegate respondsToSelector:@selector(_deviceInternalStateChanged:)]) { + [delegate _deviceInternalStateChanged:self]; + } + }]; /* END DRAGONS */ } } @@ -1070,14 +1239,15 @@ - (BOOL)_deviceUsesThread os_unfair_lock_assert_owner(&self->_lock); #ifdef DEBUG - id testDelegate = _weakDelegate.strongObject; - if (testDelegate) { - // Note: This is a hack to allow our unit tests to test the subscription pooling behavior we have implemented for thread, so we mock devices to be a thread device + // Note: This is a hack to allow our unit tests to test the subscription pooling behavior we have implemented for thread, so we mock devices to be a thread device + __block BOOL pretendThreadEnabled = NO; + [self _callFirstDelegateSynchronouslyWithBlock:^(id testDelegate) { if ([testDelegate respondsToSelector:@selector(unitTestPretendThreadEnabled:)]) { - if ([testDelegate unitTestPretendThreadEnabled:self]) { - return YES; - } + pretendThreadEnabled = [testDelegate unitTestPretendThreadEnabled:self]; } + }]; + if (pretendThreadEnabled) { + return YES; } #endif @@ -1103,14 +1273,11 @@ - (void)_clearSubscriptionPoolWork MTRAsyncWorkCompletionBlock completion = self->_subscriptionPoolWorkCompletionBlock; if (completion) { #ifdef DEBUG - id delegate = self->_weakDelegate.strongObject; - if (delegate) { - dispatch_async(self->_delegateQueue, ^{ - if ([delegate respondsToSelector:@selector(unitTestSubscriptionPoolWorkComplete:)]) { - [delegate unitTestSubscriptionPoolWorkComplete:self]; - } - }); - } + [self _callDelegatesWithBlock:^(id testDelegate) { + if ([testDelegate respondsToSelector:@selector(unitTestSubscriptionPoolWorkComplete:)]) { + [testDelegate unitTestSubscriptionPoolWorkComplete:self]; + } + }]; #endif self->_subscriptionPoolWorkCompletionBlock = nil; completion(MTRAsyncWorkComplete); @@ -1134,14 +1301,11 @@ - (void)_scheduleSubscriptionPoolWork:(dispatch_block_t)workBlock inNanoseconds: [workItem setReadyHandler:^(id _Nonnull context, NSInteger retryCount, MTRAsyncWorkCompletionBlock _Nonnull completion) { os_unfair_lock_lock(&self->_lock); #ifdef DEBUG - id delegate = self->_weakDelegate.strongObject; - if (delegate) { - dispatch_async(self->_delegateQueue, ^{ - if ([delegate respondsToSelector:@selector(unitTestSubscriptionPoolDequeue:)]) { - [delegate unitTestSubscriptionPoolDequeue:self]; - } - }); - } + [self _callDelegatesWithBlock:^(id testDelegate) { + if ([testDelegate respondsToSelector:@selector(unitTestSubscriptionPoolDequeue:)]) { + [testDelegate unitTestSubscriptionPoolDequeue:self]; + } + }]; #endif if (self->_subscriptionPoolWorkCompletionBlock) { // This means a resubscription triggering event happened and is now in-progress @@ -1224,8 +1388,7 @@ - (void)_handleSubscriptionReset:(NSNumber * _Nullable)retryDelay _lastSubscriptionFailureTime = [NSDate now]; // if there is no delegate then also do not retry - id delegate = _weakDelegate.strongObject; - if (!delegate) { + if (![self _delegateExists]) { // NOTE: Do not log anything here: we have been invalidated, and the // Matter stack might already be torn down. return; @@ -1233,7 +1396,6 @@ - (void)_handleSubscriptionReset:(NSNumber * _Nullable)retryDelay // don't schedule multiple retries if (self.reattemptingSubscription) { - MTR_LOG("%@ already reattempting subscription", self); return; } @@ -1268,9 +1430,8 @@ - (void)_handleSubscriptionReset:(NSNumber * _Nullable)retryDelay // Call _reattemptSubscriptionNowIfNeededWithReason when timer fires - if subscription is // in a better state at that time this will be a no-op. auto resubscriptionBlock = ^{ - os_unfair_lock_lock(&self->_lock); + std::lock_guard lock(self->_lock); [self _reattemptSubscriptionNowIfNeededWithReason:@"got subscription reset"]; - os_unfair_lock_unlock(&self->_lock); }; int64_t resubscriptionDelayNs = static_cast(secondsToWait * NSEC_PER_SEC); @@ -1301,14 +1462,11 @@ - (void)_handleUnsolicitedMessageFromPublisher [self _changeState:MTRDeviceStateReachable]; - id delegate = _weakDelegate.strongObject; - if (delegate) { - dispatch_async(_delegateQueue, ^{ - if ([delegate respondsToSelector:@selector(deviceBecameActive:)]) { - [delegate deviceBecameActive:self]; - } - }); - } + [self _callDelegatesWithBlock:^(id delegate) { + if ([delegate respondsToSelector:@selector(deviceBecameActive:)]) { + [delegate deviceBecameActive:self]; + } + }]; // in case this is called during exponential back off of subscription // reestablishment, this starts the attempt right away @@ -1375,6 +1533,12 @@ - (void)_persistClusterData { os_unfair_lock_assert_owner(&self->_lock); + // Sanity check + if (![self _dataStoreExists]) { + MTR_LOG_ERROR("%@ storage behavior: no data store in _persistClusterData!", self); + return; + } + // Nothing to persist if (!_clusterDataToPersist.count) { return; @@ -1403,14 +1567,11 @@ - (void)_persistClusterData _clusterDataToPersist = nil; #ifdef DEBUG - id delegate = _weakDelegate.strongObject; - if (delegate) { - dispatch_async(_delegateQueue, ^{ - if ([delegate respondsToSelector:@selector(unitTestClusterDataPersisted:)]) { - [delegate unitTestClusterDataPersisted:self]; - } - }); - } + [self _callDelegatesWithBlock:^(id testDelegate) { + if ([testDelegate respondsToSelector:@selector(unitTestClusterDataPersisted:)]) { + [testDelegate unitTestClusterDataPersisted:self]; + } + }]; #endif } @@ -1595,7 +1756,8 @@ - (void)_resetStorageBehaviorState _deviceReportingExcessivelyStartTime = nil; _reportToPersistenceDelayCurrentMultiplier = 1; - if (_persistedClusters) { + // Sanity check that there is a data + if ([self _dataStoreExists]) { [self _persistClusterData]; } } @@ -1622,13 +1784,11 @@ - (void)_handleReportEnd // After the handling of the report, if we detected a device configuration change, notify the delegate // of the same. if (_deviceConfigurationChanged) { - id delegate = _weakDelegate.strongObject; - if (delegate) { - dispatch_async(_delegateQueue, ^{ - if ([delegate respondsToSelector:@selector(deviceConfigurationChanged:)]) - [delegate deviceConfigurationChanged:self]; - }); - } + [self _callDelegatesWithBlock:^(id delegate) { + if ([delegate respondsToSelector:@selector(deviceConfigurationChanged:)]) { + [delegate deviceConfigurationChanged:self]; + } + }]; _deviceConfigurationChanged = NO; } @@ -1647,15 +1807,66 @@ - (void)_handleReportEnd // For unit testing only #ifdef DEBUG - id delegate = _weakDelegate.strongObject; - if (delegate) { - dispatch_async(_delegateQueue, ^{ - if ([delegate respondsToSelector:@selector(unitTestReportEndForDevice:)]) { - [delegate unitTestReportEndForDevice:self]; + [self _callDelegatesWithBlock:^(id testDelegate) { + if ([testDelegate respondsToSelector:@selector(unitTestReportEndForDevice:)]) { + [testDelegate unitTestReportEndForDevice:self]; + } + }]; +#endif +} + +- (BOOL)_interestedPaths:(NSArray * _Nullable)interestedPaths includesAttributePath:(MTRAttributePath *)attributePath +{ + for (id interestedPath in interestedPaths) { + if ([interestedPath isKindOfClass:[NSNumber class]]) { + NSNumber * interestedEndpointIDNumber = interestedPath; + if ([interestedEndpointIDNumber isEqualToNumber:attributePath.endpoint]) { + return YES; } - }); + } else if ([interestedPath isKindOfClass:[MTRClusterPath class]]) { + MTRClusterPath * interestedClusterPath = interestedPath; + if ([interestedClusterPath.cluster isEqualToNumber:attributePath.cluster]) { + return YES; + } + } else if ([interestedPath isKindOfClass:[MTRAttributePath class]]) { + MTRAttributePath * interestedAttributePath = interestedPath; + if (([interestedAttributePath.cluster isEqualToNumber:attributePath.cluster]) && ([interestedAttributePath.attribute isEqualToNumber:attributePath.attribute])) { + return YES; + } + } } -#endif + + return NO; +} + +// Returns filtered set of attributes using an interestedPaths array. +// Returns nil if no attribute report has a path that matches the paths in the interestedPaths array. +- (NSArray *> *)_filteredAttributes:(NSArray *> *)attributes forInterestedPaths:(NSArray * _Nullable)interestedPaths +{ + if (!interestedPaths) { + return attributes; + } + + if (!interestedPaths.count) { + return nil; + } + + NSMutableArray * filteredAttributes = nil; + for (NSDictionary * responseValue in attributes) { + MTRAttributePath * attributePath = responseValue[MTRAttributePathKey]; + if ([self _interestedPaths:interestedPaths includesAttributePath:attributePath]) { + if (!filteredAttributes) { + filteredAttributes = [NSMutableArray array]; + } + [filteredAttributes addObject:responseValue]; + } + } + + if (filteredAttributes.count && (filteredAttributes.count != attributes.count)) { + MTR_LOG("%@ filtered attribute report %lu => %lu", self, static_cast(attributes.count), static_cast(filteredAttributes.count)); + } + + return filteredAttributes; } // assume lock is held @@ -1663,12 +1874,15 @@ - (void)_reportAttributes:(NSArray *> *)attributes { os_unfair_lock_assert_owner(&self->_lock); if (attributes.count) { - id delegate = _weakDelegate.strongObject; - if (delegate) { - dispatch_async(_delegateQueue, ^{ - [delegate device:self receivedAttributeReport:attributes]; - }); - } + [self _iterateDelegatesWithBlock:^(MTRDeviceDelegateInfo * delegateInfo) { + // _iterateDelegatesWithBlock calls this with an autorelease pool, and so temporary filtered attributes reports don't bloat memory + NSArray *> * filteredAttributes = [self _filteredAttributes:attributes forInterestedPaths:delegateInfo.interestedPathsForAttributes]; + if (filteredAttributes.count) { + [delegateInfo callDelegateWithBlock:^(id delegate) { + [delegate device:self receivedAttributeReport:filteredAttributes]; + }]; + } + }]; } } @@ -1698,6 +1912,60 @@ - (void)unitTestInjectAttributeReport:(NSArray *> * } #endif +- (BOOL)_interestedPaths:(NSArray * _Nullable)interestedPaths includesEventPath:(MTREventPath *)eventPath +{ + for (id interestedPath in interestedPaths) { + if ([interestedPath isKindOfClass:[NSNumber class]]) { + NSNumber * interestedEndpointIDNumber = interestedPath; + if ([interestedEndpointIDNumber isEqualToNumber:eventPath.endpoint]) { + return YES; + } + } else if ([interestedPath isKindOfClass:[MTRClusterPath class]]) { + MTRClusterPath * interestedClusterPath = interestedPath; + if ([interestedClusterPath.cluster isEqualToNumber:eventPath.cluster]) { + return YES; + } + } else if ([interestedPath isKindOfClass:[MTREventPath class]]) { + MTREventPath * interestedEventPath = interestedPath; + if (([interestedEventPath.cluster isEqualToNumber:eventPath.cluster]) && ([interestedEventPath.event isEqualToNumber:eventPath.event])) { + return YES; + } + } + } + + return NO; +} + +// Returns filtered set of events using an interestedPaths array. +// Returns nil if no event report has a path that matches the paths in the interestedPaths array. +- (NSArray *> *)_filteredEvents:(NSArray *> *)events forInterestedPaths:(NSArray * _Nullable)interestedPaths +{ + if (!interestedPaths) { + return events; + } + + if (!interestedPaths.count) { + return nil; + } + + NSMutableArray * filteredEvents = nil; + for (NSDictionary * responseValue in events) { + MTREventPath * eventPath = responseValue[MTREventPathKey]; + if ([self _interestedPaths:interestedPaths includesEventPath:eventPath]) { + if (!filteredEvents) { + filteredEvents = [NSMutableArray array]; + } + [filteredEvents addObject:responseValue]; + } + } + + if (filteredEvents.count && (filteredEvents.count != events.count)) { + MTR_LOG("%@ filtered event report %lu => %lu", self, static_cast(events.count), static_cast(filteredEvents.count)); + } + + return filteredEvents; +} + - (void)_handleEventReport:(NSArray *> *)eventReport { std::lock_guard lock(_lock); @@ -1784,12 +2052,19 @@ - (void)_handleEventReport:(NSArray *> *)eventRepor MTR_LOG("%@ updated estimated start time to %@", self, _estimatedStartTime); } - id delegate = _weakDelegate.strongObject; - if (delegate) { + __block BOOL delegatesCalled = NO; + [self _iterateDelegatesWithBlock:^(MTRDeviceDelegateInfo * delegateInfo) { + // _iterateDelegatesWithBlock calls this with an autorelease pool, and so temporary filtered event reports don't bloat memory + NSArray *> * filteredEvents = [self _filteredEvents:reportToReturn forInterestedPaths:delegateInfo.interestedPathsForEvents]; + if (filteredEvents.count) { + [delegateInfo callDelegateWithBlock:^(id delegate) { + [delegate device:self receivedEventReport:filteredEvents]; + }]; + delegatesCalled = YES; + } + }]; + if (delegatesCalled) { _unreportedEvents = nil; - dispatch_async(_delegateQueue, ^{ - [delegate device:self receivedEventReport:reportToReturn]; - }); } else { // save unreported events _unreportedEvents = reportToReturn; @@ -2017,13 +2292,15 @@ - (void)_setupSubscriptionWithReason:(NSString *)reason } #ifdef DEBUG - id delegate = _weakDelegate.strongObject; + __block NSNumber * delegateMin = nil; Optional maxIntervalOverride; - if (delegate) { - if ([delegate respondsToSelector:@selector(unitTestMaxIntervalOverrideForSubscription:)]) { - NSNumber * delegateMin = [delegate unitTestMaxIntervalOverrideForSubscription:self]; - maxIntervalOverride.Emplace(delegateMin.unsignedIntValue); + [self _callFirstDelegateSynchronouslyWithBlock:^(id testDelegate) { + if ([testDelegate respondsToSelector:@selector(unitTestMaxIntervalOverrideForSubscription:)]) { + delegateMin = [testDelegate unitTestMaxIntervalOverrideForSubscription:self]; } + }]; + if (delegateMin) { + maxIntervalOverride.Emplace(delegateMin.unsignedIntValue); } #endif @@ -2036,21 +2313,23 @@ - (void)_setupSubscriptionWithReason:(NSString *)reason MTR_LOG("%@ setting up subscription with reason: %@", self, reason); - bool markUnreachableAfterWait = true; + __block bool markUnreachableAfterWait = true; #ifdef DEBUG - if (delegate && [delegate respondsToSelector:@selector(unitTestSuppressTimeBasedReachabilityChanges:)]) { - markUnreachableAfterWait = ![delegate unitTestSuppressTimeBasedReachabilityChanges:self]; - } + [self _callFirstDelegateSynchronouslyWithBlock:^(id testDelegate) { + if ([testDelegate respondsToSelector:@selector(unitTestSuppressTimeBasedReachabilityChanges:)]) { + markUnreachableAfterWait = ![testDelegate unitTestSuppressTimeBasedReachabilityChanges:self]; + } + }]; #endif if (markUnreachableAfterWait) { // Set up a timer to mark as not reachable if it takes too long to set up a subscription - MTRWeakReference * weakSelf = [MTRWeakReference weakReferenceWithObject:self]; + mtr_weakify(self); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, static_cast(kSecondsToWaitBeforeMarkingUnreachableAfterSettingUpSubscription) * static_cast(NSEC_PER_SEC)), self.queue, ^{ - MTRDevice * strongSelf = weakSelf.strongObject; - if (strongSelf != nil) { - std::lock_guard lock(strongSelf->_lock); - [strongSelf _markDeviceAsUnreachableIfNeverSubscribed]; + mtr_strongify(self); + if (self != nil) { + std::lock_guard lock(self->_lock); + [self _markDeviceAsUnreachableIfNeverSubscribed]; } }); } @@ -2255,6 +2534,20 @@ - (NSUInteger)unitTestAttributesReportedSinceLastCheck _unitTestAttributesReportedSinceLastCheck = 0; return attributesReportedSinceLastCheck; } + +- (NSUInteger)unitTestNonnullDelegateCount +{ + std::lock_guard lock(self->_lock); + + NSUInteger nonnullDelegateCount = 0; + for (MTRDeviceDelegateInfo * delegateInfo in _delegates) { + if (delegateInfo.delegate) { + nonnullDelegateCount++; + } + } + + return nonnullDelegateCount; +} #endif #pragma mark Device Interactions @@ -2549,14 +2842,15 @@ - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID attributeID:attributeID]; - BOOL useValueAsExpectedValue = YES; + __block BOOL useValueAsExpectedValue = YES; #ifdef DEBUG os_unfair_lock_lock(&self->_lock); - id delegate = _weakDelegate.strongObject; + [self _callFirstDelegateSynchronouslyWithBlock:^(id delegate) { + if ([delegate respondsToSelector:@selector(unitTestShouldSkipExpectedValuesForWrite:)]) { + useValueAsExpectedValue = ![delegate unitTestShouldSkipExpectedValuesForWrite:self]; + } + }]; os_unfair_lock_unlock(&self->_lock); - if ([delegate respondsToSelector:@selector(unitTestShouldSkipExpectedValuesForWrite:)]) { - useValueAsExpectedValue = ![delegate unitTestShouldSkipExpectedValuesForWrite:self]; - } #endif uint64_t expectedValueID = 0; @@ -2949,10 +3243,10 @@ - (void)_checkExpiredExpectedValues if (waitTime < MTR_DEVICE_EXPIRATION_CHECK_TIMER_MINIMUM_WAIT_TIME) { waitTime = MTR_DEVICE_EXPIRATION_CHECK_TIMER_MINIMUM_WAIT_TIME; } - MTRWeakReference * weakSelf = [MTRWeakReference weakReferenceWithObject:self]; + mtr_weakify(self); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (waitTime * NSEC_PER_SEC)), self.queue, ^{ - MTRDevice * strongSelf = weakSelf.strongObject; - [strongSelf _performScheduledExpirationCheck]; + mtr_strongify(self); + [self _performScheduledExpirationCheck]; }); } } @@ -3232,7 +3526,11 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray *> * data) { + [gotAReport2 fulfill]; + __strong __auto_type strongDelegate = weakDelegate2; + strongDelegate.onAttributeDataReceived = nil; + }; + + [device addDelegate:delegate2 queue:queue]; + + // Wait just long enough for 1 report + [self waitForExpectations:@[ gotAReport2 ] timeout:60]; + + // Verify that at this point MTRDevice is still seeing 2 delegates + XCTAssertEqual([device unitTestNonnullDelegateCount], 2); + } + + [self waitForExpectations:@[ gotReportEnd1 ] timeout:60]; + + // Verify that once the entire report comes in from all-clusters, that delegate2 had been dealloced, and MTRDevice no longer sees it + XCTAssertEqual([device unitTestNonnullDelegateCount], 1); +} + +- (NSDictionary *)_testAttributeResponseValueWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID value:(unsigned int)testValue +{ + return @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID], + MTRDataKey : @ { + MTRDataVersionKey : @(testValue), + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(testValue), + } + }; +} + +- (NSDictionary *)_testEventResponseValueWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID eventID:(NSNumber *)eventID +{ + return @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:endpointID clusterID:clusterID eventID:eventID], + MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate), + MTREventTimestampDateKey : [NSDate date], + // For unit test no real data is needed, but timestamp is required + }; +} +- (void)test038_MTRDeviceMultipleDelegatesInterestedPaths +{ + dispatch_queue_t queue = dispatch_get_main_queue(); + + // First start with clean slate by removing the MTRDevice and clearing the persisted cache + __auto_type * device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController]; + [sController removeDevice:device]; + [sController.controllerDataStore clearAllStoredClusterData]; + NSDictionary * storedClusterDataAfterClear = [sController.controllerDataStore getStoredClusterDataForNodeID:@(kDeviceId)]; + XCTAssertEqual(storedClusterDataAfterClear.count, 0); + + // Now recreate device and get subscription primed + device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController]; + XCTestExpectation * gotReportEnd1 = [self expectationWithDescription:@"Report end for delegate 1"]; + + __auto_type * delegate1 = [[MTRDeviceTestDelegateWithSubscriptionSetupOverride alloc] init]; + delegate1.skipSetupSubscription = YES; + __weak __auto_type weakDelegate1 = delegate1; + __block NSUInteger attributesReceived1 = 0; + delegate1.onAttributeDataReceived = ^(NSArray *> * data) { + attributesReceived1 += data.count; + }; + __block NSUInteger eventsReceived1 = 0; + delegate1.onEventDataReceived = ^(NSArray *> * data) { + eventsReceived1 += data.count; + }; + delegate1.onReportEnd = ^{ + [gotReportEnd1 fulfill]; + __strong __auto_type strongDelegate = weakDelegate1; + strongDelegate.onReportEnd = nil; + }; + + // All 9 attributes from endpoint 1, plus 3 from endpoint 2, plus endpoint 3 = total 21 + NSArray * interestedAttributePaths1 = @[ + [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(11) attributeID:@(111)], + [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(11) attributeID:@(112)], + [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(11) attributeID:@(113)], + [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(12) attributeID:@(121)], + [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(12) attributeID:@(122)], + [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(12) attributeID:@(123)], + [MTRClusterPath clusterPathWithEndpointID:@(1) clusterID:@(13)], + [MTRAttributePath attributePathWithEndpointID:@(2) clusterID:@(21) attributeID:@(211)], + [MTRAttributePath attributePathWithEndpointID:@(2) clusterID:@(21) attributeID:@(212)], + [MTRClusterPath clusterPathWithEndpointID:@(2) clusterID:@(21)], + @(3), + ]; + // All 9 event from endpoint 1, plus 3 from endpoint 2, plus endpoint 3 = total 21 + NSArray * interestedEventPaths1 = @[ + [MTREventPath eventPathWithEndpointID:@(1) clusterID:@(11) eventID:@(111)], + [MTREventPath eventPathWithEndpointID:@(1) clusterID:@(11) eventID:@(112)], + [MTREventPath eventPathWithEndpointID:@(1) clusterID:@(11) eventID:@(113)], + [MTREventPath eventPathWithEndpointID:@(1) clusterID:@(12) eventID:@(121)], + [MTREventPath eventPathWithEndpointID:@(1) clusterID:@(12) eventID:@(122)], + [MTREventPath eventPathWithEndpointID:@(1) clusterID:@(12) eventID:@(123)], + [MTRClusterPath clusterPathWithEndpointID:@(1) clusterID:@(13)], + [MTREventPath eventPathWithEndpointID:@(2) clusterID:@(21) eventID:@(211)], + [MTREventPath eventPathWithEndpointID:@(2) clusterID:@(21) eventID:@(212)], + [MTRClusterPath clusterPathWithEndpointID:@(2) clusterID:@(21)], + @(3), + ]; + [device addDelegate:delegate1 queue:queue interestedPathsForAttributes:interestedAttributePaths1 interestedPathsForEvents:interestedEventPaths1]; + + // Delegate 2 + XCTestExpectation * gotReportEnd2 = [self expectationWithDescription:@"Report end for delegate 2"]; + __auto_type * delegate2 = [[MTRDeviceTestDelegateWithSubscriptionSetupOverride alloc] init]; + delegate2.skipSetupSubscription = YES; + __weak __auto_type weakDelegate2 = delegate2; + __block NSUInteger attributesReceived2 = 0; + delegate2.onAttributeDataReceived = ^(NSArray *> * data) { + attributesReceived2 += data.count; + }; + __block NSUInteger eventsReceived2 = 0; + delegate2.onEventDataReceived = ^(NSArray *> * data) { + eventsReceived2 += data.count; + }; + delegate2.onReportEnd = ^{ + [gotReportEnd2 fulfill]; + __strong __auto_type strongDelegate = weakDelegate2; + strongDelegate.onReportEnd = nil; + }; + + // All 9 attributes from endpoint 3 + NSArray * interestedAttributePaths2 = @[ + [MTRClusterPath clusterPathWithEndpointID:@(3) clusterID:@(31)], + [MTRClusterPath clusterPathWithEndpointID:@(3) clusterID:@(32)], + [MTRClusterPath clusterPathWithEndpointID:@(3) clusterID:@(33)], + ]; + // Test empty events (all filtered) + [device addDelegate:delegate2 queue:queue interestedPathsForAttributes:interestedAttributePaths2 interestedPathsForEvents:@[]]; + + // Delegate 3 + XCTestExpectation * gotReportEnd3 = [self expectationWithDescription:@"Report end for delegate 3"]; + __auto_type * delegate3 = [[MTRDeviceTestDelegateWithSubscriptionSetupOverride alloc] init]; + delegate3.skipSetupSubscription = YES; + __weak __auto_type weakDelegate3 = delegate3; + __block NSUInteger attributesReceived3 = 0; + delegate3.onAttributeDataReceived = ^(NSArray *> * data) { + attributesReceived3 += data.count; + }; + __block NSUInteger eventsReceived3 = 0; + delegate3.onEventDataReceived = ^(NSArray *> * data) { + eventsReceived3 += data.count; + }; + delegate3.onReportEnd = ^{ + [gotReportEnd3 fulfill]; + __strong __auto_type strongDelegate = weakDelegate3; + strongDelegate.onReportEnd = nil; + }; + + // All 9 events from endpoint 4 + NSArray * interestedEventPaths3 = @[ + [MTRClusterPath clusterPathWithEndpointID:@(4) clusterID:@(41)], + [MTRClusterPath clusterPathWithEndpointID:@(4) clusterID:@(42)], + [MTRClusterPath clusterPathWithEndpointID:@(4) clusterID:@(43)], + ]; + // Test empty attributes (all filtered) + [device addDelegate:delegate3 queue:queue interestedPathsForAttributes:@[] interestedPathsForEvents:interestedEventPaths3]; + + // Delegate 4 + XCTestExpectation * gotReportEnd4 = [self expectationWithDescription:@"Report end for delegate 4"]; + __auto_type * delegate4 = [[MTRDeviceTestDelegateWithSubscriptionSetupOverride alloc] init]; + delegate3.skipSetupSubscription = YES; + __weak __auto_type weakDelegate4 = delegate4; + __block NSUInteger attributesReceived4 = 0; + delegate4.onAttributeDataReceived = ^(NSArray *> * data) { + attributesReceived4 += data.count; + }; + __block NSUInteger eventsReceived4 = 0; + delegate4.onEventDataReceived = ^(NSArray *> * data) { + eventsReceived4 += data.count; + }; + delegate4.onReportEnd = ^{ + [gotReportEnd4 fulfill]; + __strong __auto_type strongDelegate = weakDelegate4; + strongDelegate.onReportEnd = nil; + }; + + // Test a fourth delegate that receives everything will get all the reports + [device addDelegate:delegate4 queue:queue]; + + // Inject events first + NSMutableArray * eventReport = [NSMutableArray array]; + // Construct 36 events with endpoints 1~4, clusters 11 ~ 33, and events 111~333 + for (int i = 1; i <= 4; i++) { + for (int j = 1; j <= 3; j++) { + for (int k = 1; k <= 3; k++) { + int endpointID = i; + int clusterID = i * 10 + j; + int eventID = i * 100 + j * 10 + k; + [eventReport addObject:[self _testEventResponseValueWithEndpointID:@(endpointID) clusterID:@(clusterID) eventID:@(eventID)]]; + } + } + } + [device unitTestInjectEventReport:eventReport]; + + // Now inject attributes and check that each delegate gets the right set of attributes + NSMutableArray * attributeReport = [NSMutableArray array]; + // Construct 36 attributes with endpoints 1~4, clusters 11 ~ 33, and attributes 111~333 + for (int i = 1; i <= 4; i++) { + for (int j = 1; j <= 3; j++) { + for (int k = 1; k <= 3; k++) { + int endpointID = i; + int clusterID = i * 10 + j; + int attributeID = i * 100 + j * 10 + k; + int value = attributeID + 10000; + [attributeReport addObject:[self _testAttributeResponseValueWithEndpointID:@(endpointID) clusterID:@(clusterID) attributeID:@(attributeID) value:value]]; + } + } + } + [device unitTestInjectAttributeReport:attributeReport fromSubscription:YES]; + + [self waitForExpectations:@[ gotReportEnd1, gotReportEnd2, gotReportEnd3, gotReportEnd4 ] timeout:60]; + + XCTAssertEqual(attributesReceived1, 21); + XCTAssertEqual(eventsReceived1, 21); + XCTAssertEqual(attributesReceived2, 9); + XCTAssertEqual(eventsReceived2, 0); + XCTAssertEqual(attributesReceived3, 0); + XCTAssertEqual(eventsReceived3, 9); + XCTAssertEqual(attributesReceived4, 36); + XCTAssertEqual(eventsReceived4, 36); + + // Now reset the counts, remove delegate1 and verify that only delegates 2~4 got reports + attributesReceived1 = 0; + eventsReceived1 = 0; + attributesReceived2 = 0; + eventsReceived2 = 0; + attributesReceived3 = 0; + eventsReceived3 = 0; + attributesReceived4 = 0; + eventsReceived4 = 0; + [device removeDelegate:delegate1]; + + XCTestExpectation * gotReportEnd2again = [self expectationWithDescription:@"Report end for delegate 2 again"]; + delegate2.onReportEnd = ^{ + [gotReportEnd2again fulfill]; + __strong __auto_type strongDelegate = weakDelegate2; + strongDelegate.onReportEnd = nil; + }; + XCTestExpectation * gotReportEnd3again = [self expectationWithDescription:@"Report end for delegate 3 again"]; + delegate3.onReportEnd = ^{ + [gotReportEnd3again fulfill]; + __strong __auto_type strongDelegate = weakDelegate3; + strongDelegate.onReportEnd = nil; + }; + XCTestExpectation * gotReportEnd4again = [self expectationWithDescription:@"Report end for delegate 4 again"]; + delegate4.onReportEnd = ^{ + [gotReportEnd4again fulfill]; + __strong __auto_type strongDelegate = weakDelegate4; + strongDelegate.onReportEnd = nil; + }; + + // Construct 36 new events with new timestamps + [eventReport removeAllObjects]; + for (int i = 1; i <= 4; i++) { + for (int j = 1; j <= 3; j++) { + for (int k = 1; k <= 3; k++) { + int endpointID = i; + int clusterID = i * 10 + j; + int eventID = i * 100 + j * 10 + k; + [eventReport addObject:[self _testEventResponseValueWithEndpointID:@(endpointID) clusterID:@(clusterID) eventID:@(eventID)]]; + } + } + } + [device unitTestInjectEventReport:eventReport]; + + // Construct 36 new attributes with new values / data versions + [attributeReport removeAllObjects]; + for (int i = 1; i <= 4; i++) { + for (int j = 1; j <= 3; j++) { + for (int k = 1; k <= 3; k++) { + int endpointID = i; + int clusterID = i * 10 + j; + int attributeID = i * 100 + j * 10 + k; + int value = attributeID + 20000; + [attributeReport addObject:[self _testAttributeResponseValueWithEndpointID:@(endpointID) clusterID:@(clusterID) attributeID:@(attributeID) value:value]]; + } + } + } + [device unitTestInjectAttributeReport:attributeReport fromSubscription:YES]; + [self waitForExpectations:@[ gotReportEnd2again, gotReportEnd3again, gotReportEnd4again ] timeout:60]; + + XCTAssertEqual(attributesReceived1, 0); + XCTAssertEqual(eventsReceived1, 0); + XCTAssertEqual(attributesReceived2, 9); + XCTAssertEqual(eventsReceived2, 0); + XCTAssertEqual(attributesReceived3, 0); + XCTAssertEqual(eventsReceived3, 9); + XCTAssertEqual(attributesReceived4, 36); + XCTAssertEqual(eventsReceived4, 36); +} + @end @interface MTRDeviceEncoderTests : XCTestCase diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h index c18daf6199d8e3..0705cb89cb1ff6 100644 --- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h +++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h @@ -78,6 +78,7 @@ NS_ASSUME_NONNULL_BEGIN reportToPersistenceDelayMaxMultiplier:(double)reportToPersistenceDelayMaxMultiplier deviceReportingExcessivelyIntervalThreshold:(NSTimeInterval)deviceReportingExcessivelyIntervalThreshold; - (void)unitTestSetMostRecentReportTimes:(NSMutableArray *)mostRecentReportTimes; +- (NSUInteger)unitTestNonnullDelegateCount; @end #endif