diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 98af8fb4ab16c2..eb16b511e2a7b1 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -209,6 +209,11 @@ - (void)storeValue:(MTRDeviceDataValueDictionary _Nullable)value forAttribute:(N _attributes[attribute] = value; } +- (void)removeValueForAttribute:(NSNumber *)attribute +{ + [_attributes removeObjectForKey:attribute]; +} + - (NSDictionary *)attributes { return _attributes; @@ -670,7 +675,7 @@ - (void)_setDSTOffsets:(NSArray completion:setDSTOffsetResponseHandler]; } -- (NSMutableArray *)arrayOfNumbersFromAttributeValue:(NSDictionary *)dataDictionary +- (NSMutableArray *)arrayOfNumbersFromAttributeValue:(MTRDeviceDataValueDictionary)dataDictionary { if (![MTRArrayValueType isEqual:dataDictionary[MTRTypeKey]]) { return nil; @@ -1892,6 +1897,17 @@ - (void)_setCachedAttributeValue:(MTRDeviceDataValueDictionary _Nullable)value f _clusterDataToPersist[clusterPath] = clusterData; } +- (void)_removeCachedAttribute:(NSNumber *)attributeID fromCluster:(MTRClusterPath *)clusterPath +{ + os_unfair_lock_assert_owner(&self->_lock); + + if (_clusterDataToPersist == nil) { + return; + } + auto * clusterData = _clusterDataToPersist[clusterPath]; + [clusterData removeValueForAttribute:attributeID]; +} + - (void)_createDataVersionFilterListFromDictionary:(NSDictionary *)dataVersions dataVersionFilterList:(DataVersionFilter **)dataVersionFilterList count:(size_t *)count sizeReduction:(size_t)sizeReduction { size_t maxDataVersionFilterSize = dataVersions.count; @@ -2956,7 +2972,7 @@ - (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary } // Utility to return data value dictionary without data version -- (NSDictionary *)_dataValueWithoutDataVersion:(NSDictionary *)attributeValue; +- (NSDictionary *)_dataValueWithoutDataVersion:(NSDictionary *)attributeValue { // Sanity check for nil - return the same input to fail gracefully if (!attributeValue || !attributeValue[MTRTypeKey]) { @@ -3018,6 +3034,116 @@ - (BOOL)_attributeAffectsDeviceConfiguration:(MTRAttributePath *)attributePath return NO; } +- (void)_removeClusters:(NSSet *)clusterPathsToRemove + doRemoveFromDataStore:(BOOL)doRemoveFromDataStore +{ + os_unfair_lock_assert_owner(&self->_lock); + + [_persistedClusters minusSet:clusterPathsToRemove]; + + for (MTRClusterPath * path in clusterPathsToRemove) { + [_persistedClusterData removeObjectForKey:path]; + [_clusterDataToPersist removeObjectForKey:path]; + if (doRemoveFromDataStore) { + [self.deviceController.controllerDataStore clearStoredClusterDataForNodeID:self.nodeID endpointID:path.endpoint clusterID:path.cluster]; + } + } +} + +- (void)_removeAttributes:(NSSet *)attributes fromCluster:(MTRClusterPath *)clusterPath +{ + os_unfair_lock_assert_owner(&self->_lock); + + for (NSNumber * attribute in attributes) { + [self _removeCachedAttribute:attribute fromCluster:clusterPath]; + } + // Just clear out the NSCache entry for this cluster, so we'll load it from storage as needed. + [_persistedClusterData removeObjectForKey:clusterPath]; + [self.deviceController.controllerDataStore removeAttributes:attributes fromCluster:clusterPath forNodeID:self.nodeID]; +} + +- (void)_pruneEndpointsIn:(MTRDeviceDataValueDictionary)previousPartsListValue + missingFrom:(MTRDeviceDataValueDictionary)newPartsListValue +{ + // If the parts list changed and one or more endpoints were removed, remove all the + // clusters for all those endpoints from our data structures. + // Also remove those endpoints from the data store. + NSMutableSet * toBeRemovedEndpoints = [NSMutableSet setWithArray:[self arrayOfNumbersFromAttributeValue:previousPartsListValue]]; + NSSet * endpointsOnDevice = [NSSet setWithArray:[self arrayOfNumbersFromAttributeValue:newPartsListValue]]; + [toBeRemovedEndpoints minusSet:endpointsOnDevice]; + + for (NSNumber * endpoint in toBeRemovedEndpoints) { + NSMutableSet * clusterPathsToRemove = [[NSMutableSet alloc] init]; + for (MTRClusterPath * path in _persistedClusters) { + if ([path.endpoint isEqualToNumber:endpoint]) { + [clusterPathsToRemove addObject:path]; + } + } + [self _removeClusters:clusterPathsToRemove doRemoveFromDataStore:NO]; + [self.deviceController.controllerDataStore clearStoredClusterDataForNodeID:self.nodeID endpointID:endpoint]; + } +} + +- (void)_pruneClustersIn:(MTRDeviceDataValueDictionary)previousServerListValue + missingFrom:(MTRDeviceDataValueDictionary)newServerListValue + forEndpoint:(NSNumber *)endpointID +{ + // If the server list changed and clusters were removed, remove those clusters from our data structures. + // Also remove them from the data store. + NSMutableSet * toBeRemovedClusters = [NSMutableSet setWithArray:[self arrayOfNumbersFromAttributeValue:previousServerListValue]]; + NSSet * clustersStillOnEndpoint = [NSSet setWithArray:[self arrayOfNumbersFromAttributeValue:newServerListValue]]; + [toBeRemovedClusters minusSet:clustersStillOnEndpoint]; + + NSMutableSet * clusterPathsToRemove = [[NSMutableSet alloc] init]; + for (MTRClusterPath * path in _persistedClusters) { + if ([path.endpoint isEqualToNumber:endpointID] && [toBeRemovedClusters containsObject:path.cluster]) + [clusterPathsToRemove addObject:path]; + } + [self _removeClusters:clusterPathsToRemove doRemoveFromDataStore:YES]; +} + +- (void)_pruneAttributesIn:(MTRDeviceDataValueDictionary)previousAttributeListValue + missingFrom:(MTRDeviceDataValueDictionary)newAttributeListValue + forCluster:(MTRClusterPath *)clusterPath +{ + // If the attribute list changed and attributes were removed, remove the attributes from our + // data structures. + NSMutableSet * toBeRemovedAttributes = [NSMutableSet setWithArray:[self arrayOfNumbersFromAttributeValue:previousAttributeListValue]]; + NSSet * attributesStillInCluster = [NSSet setWithArray:[self arrayOfNumbersFromAttributeValue:newAttributeListValue]]; + + [toBeRemovedAttributes minusSet:attributesStillInCluster]; + [self _removeAttributes:toBeRemovedAttributes fromCluster:clusterPath]; +} + +- (void)_pruneStoredDataForPath:(MTRAttributePath *)attributePath + missingFrom:(MTRDeviceDataValueDictionary)newAttributeDataValue +{ + os_unfair_lock_assert_owner(&self->_lock); + + if (![self _dataStoreExists] && !_clusterDataToPersist.count) { + MTR_LOG_DEBUG("%@ No data store to prune from", self); + return; + } + + // Check if parts list changed or server list changed for the descriptor cluster or the attribute list changed for a cluster. + // If yes, we might need to prune any deleted endpoints, clusters or attributes from the storage and persisted cluster data. + if (attributePath.cluster.unsignedLongValue == MTRClusterIDTypeDescriptorID) { + if (attributePath.attribute.unsignedLongValue == MTRAttributeIDTypeClusterDescriptorAttributePartsListID && [attributePath.endpoint isEqualToNumber:@(kRootEndpointId)]) { + [self _pruneEndpointsIn:[self _cachedAttributeValueForPath:attributePath] missingFrom:newAttributeDataValue]; + return; + } + + if (attributePath.attribute.unsignedLongValue == MTRAttributeIDTypeClusterDescriptorAttributeServerListID) { + [self _pruneClustersIn:[self _cachedAttributeValueForPath:attributePath] missingFrom:newAttributeDataValue forEndpoint:attributePath.endpoint]; + return; + } + } + + if (attributePath.attribute.unsignedLongValue == MTRAttributeIDTypeGlobalAttributeAttributeListID) { + [self _pruneAttributesIn:[self _cachedAttributeValueForPath:attributePath] missingFrom:newAttributeDataValue forCluster:[MTRClusterPath clusterPathWithEndpointID:attributePath.endpoint clusterID:attributePath.cluster]]; + } +} + // assume lock is held - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray *> *)reportedAttributeValues fromSubscription:(BOOL)isFromSubscription { @@ -3071,13 +3197,16 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray *)clusterData forNodeID:(NSNumber *)nodeID; - (void)clearStoredClusterDataForNodeID:(NSNumber *)nodeID; +- (void)clearStoredClusterDataForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID; +- (void)clearStoredClusterDataForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID; +- (void)removeAttributes:(NSSet *)attributes fromCluster:(MTRClusterPath *)path forNodeID:(NSNumber *)nodeID; - (void)clearAllStoredClusterData; /** diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm index c96de65d39eb84..d38712c00a5151 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm @@ -446,6 +446,23 @@ - (BOOL)_storeEndpointIndex:(NSArray *)endpointIndex forNodeID:(NSNu return [self _storeAttributeCacheValue:endpointIndex forKey:[self _endpointIndexKeyForNodeID:nodeID]]; } +- (BOOL)_removeEndpointFromEndpointIndex:(NSNumber *)endpointID forNodeID:(NSNumber *)nodeID +{ + dispatch_assert_queue(_storageDelegateQueue); + if (!endpointID || !nodeID) { + MTR_LOG_ERROR("%s: unexpected nil input", __func__); + return NO; + } + + NSMutableArray * endpointIndex = [[self _fetchEndpointIndexForNodeID:nodeID] mutableCopy]; + if (endpointIndex == nil) { + return NO; + } + + [endpointIndex removeObject:endpointID]; + return [self _storeEndpointIndex:endpointIndex forNodeID:nodeID]; +} + - (BOOL)_deleteEndpointIndexForNodeID:(NSNumber *)nodeID { dispatch_assert_queue(_storageDelegateQueue); @@ -699,6 +716,76 @@ - (void)clearStoredClusterDataForNodeID:(NSNumber *)nodeID }); } +- (void)clearStoredClusterDataForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID +{ + dispatch_async(_storageDelegateQueue, ^{ + NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID]; + NSMutableArray * clusterIndexCopy = [clusterIndex mutableCopy]; + [clusterIndexCopy removeObject:clusterID]; + + BOOL success; + if (clusterIndexCopy.count != clusterIndex.count) { + success = [self _storeClusterIndex:clusterIndexCopy forNodeID:nodeID endpointID:endpointID]; + if (!success) { + MTR_LOG_ERROR("clearStoredClusterDataForNodeID: _storeClusterIndex failed for node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); + } + } + + success = [self _deleteClusterDataForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + if (!success) { + MTR_LOG_ERROR("clearStoredClusterDataForNodeID: _deleteClusterDataForNodeID failed for node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue); + return; + } + + MTR_LOG("clearStoredClusterDataForNodeID: Deleted endpoint %u cluster 0x%08lX for node 0x%016llX successfully", endpointID.unsignedShortValue, clusterID.unsignedLongValue, nodeID.unsignedLongLongValue); + }); +} + +- (void)clearStoredClusterDataForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID +{ + dispatch_async(_storageDelegateQueue, ^{ + BOOL success = [self _removeEndpointFromEndpointIndex:endpointID forNodeID:nodeID]; + if (!success) { + MTR_LOG_ERROR("removeEndpointFromEndpointIndex for endpointID %u failed for node 0x%016llX", endpointID.unsignedShortValue, nodeID.unsignedLongLongValue); + } + + NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID]; + + for (NSNumber * cluster in clusterIndex) { + success = [self _deleteClusterDataForNodeID:nodeID endpointID:endpointID clusterID:cluster]; + if (!success) { + MTR_LOG_ERROR("Delete failed for clusterData for node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, cluster.unsignedLongValue); + } + } + + success = [self _deleteClusterIndexForNodeID:nodeID endpointID:endpointID]; + if (!success) { + MTR_LOG_ERROR("Delete failed for clusterIndex for node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); + } + + MTR_LOG("clearStoredClusterDataForNodeID: Deleted endpoint %u for node 0x%016llX successfully", endpointID.unsignedShortValue, nodeID.unsignedLongLongValue); + }); +} + +- (void)removeAttributes:(NSSet *)attributes fromCluster:(MTRClusterPath *)path forNodeID:(NSNumber *)nodeID +{ + MTRDeviceClusterData * clusterData = [self getStoredClusterDataForNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster]; + if (clusterData == nil) { + return; + } + for (NSNumber * attribute in attributes) { + [clusterData removeValueForAttribute:attribute]; + } + + dispatch_async(_storageDelegateQueue, ^{ + BOOL success = [self _storeClusterData:clusterData forNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster]; + if (!success) { + MTR_LOG_ERROR("removeAttributes: _storeClusterData failed for node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, path.endpoint.unsignedShortValue); + } + MTR_LOG("removeAttributes: Deleted attributes %@ from endpoint %u cluster 0x%08lX for node 0x%016llX successfully", attributes, path.endpoint.unsignedShortValue, path.cluster.unsignedLongValue, nodeID.unsignedLongLongValue); + }); +} + - (void)clearAllStoredClusterData { dispatch_async(_storageDelegateQueue, ^{ diff --git a/src/darwin/Framework/CHIP/MTRDevice_Internal.h b/src/darwin/Framework/CHIP/MTRDevice_Internal.h index a9647122d4fba2..2ad2f6422dd9e7 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDevice_Internal.h @@ -60,6 +60,7 @@ MTR_TESTABLE @property (nonatomic, readonly) NSDictionary * attributes; // attributeID => data-value dictionary - (void)storeValue:(MTRDeviceDataValueDictionary _Nullable)value forAttribute:(NSNumber *)attribute; +- (void)removeValueForAttribute:(NSNumber *)attribute; - (nullable instancetype)initWithDataVersion:(NSNumber * _Nullable)dataVersion attributes:(NSDictionary * _Nullable)attributes; @end diff --git a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m index 52b1ae36673b28..fc233ffaebf93d 100644 --- a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m +++ b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m @@ -2298,4 +2298,487 @@ - (void)testSubscriptionPool [self doTestSubscriptionPoolWithSize:2 deviceOnboardingPayloads:deviceOnboardingPayloads]; } +- (MTRDevice *)getMTRDevice:(NSNumber *)deviceID +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + + NSNumber * nodeID = @(123); + NSNumber * fabricID = @(456); + + NSError * error; + __auto_type * storageDelegate = [[MTRTestPerControllerStorageWithBulkReadWrite alloc] initWithControllerID:[NSUUID UUID]]; + MTRPerControllerStorageTestsCertificateIssuer * certificateIssuer; + MTRDeviceController * controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + error:&error + certificateIssuer:&certificateIssuer]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID); + + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + MTRDevice * device = [MTRDevice deviceWithNodeID:deviceID controller:controller]; + return device; +} + +- (NSMutableArray *)getEndpointArrayFromPartsList:(MTRDeviceDataValueDictionary)partsList forDevice:(MTRDevice *)device +{ + // Initialize the endpoint array with endpoint 0. + NSMutableArray * endpoints = [NSMutableArray arrayWithObject:@0]; + + [endpoints addObjectsFromArray:[device arrayOfNumbersFromAttributeValue:partsList]]; + return endpoints; +} + +- (void)testDataStorageUpdatesWhenRemovingEndpoints +{ + NSNumber * deviceID = @(17); + __auto_type * device = [self getMTRDevice:deviceID]; + __auto_type queue = dispatch_get_main_queue(); + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + __auto_type * controller = device.deviceController; + + XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"]; + + __block NSNumber * dataVersionForPartsList; + __block NSNumber * rootEndpoint = @0; + + // This test will do the following - + // 1. Get the data version and attribute value of the parts list for endpoint 0 to inject a fake report. The attribute report will delete endpoint 2. + // That should cause the endpoint and its corresponding clusters to be removed from data storage. + // 2. The data store is populated with cluster index and cluster data for endpoints 0, 1 and 2 initially. + // 3. After the fake attribute report is injected with deleted endpoint 2, make sure the data store is still populated with cluster index and cluster data + // for endpoints 0 and 1 but not 2. + __block MTRDeviceDataValueDictionary testDataForPartsList; + __block id testClusterDataValueForPartsList; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + XCTAssertGreaterThan(attributeReport.count, 0); + + for (NSDictionary * attributeDict in attributeReport) { + MTRAttributePath * attributePath = attributeDict[MTRAttributePathKey]; + XCTAssertNotNil(attributePath); + + if ([attributePath.endpoint isEqualToNumber:rootEndpoint] && attributePath.cluster.unsignedLongValue == MTRClusterIDTypeDescriptorID && attributePath.attribute.unsignedLongValue == MTRAttributeIDTypeClusterDescriptorAttributePartsListID) { + testDataForPartsList = attributeDict[MTRDataKey]; + XCTAssertNotNil(testDataForPartsList); + dataVersionForPartsList = testDataForPartsList[MTRDataVersionKey]; + id dataValue = testDataForPartsList[MTRValueKey]; + XCTAssertNotNil(dataValue); + testClusterDataValueForPartsList = [dataValue mutableCopy]; + } + } + }; + + __block NSMutableDictionary *> * initialClusterIndex = [[NSMutableDictionary alloc] init]; + __block NSMutableArray * testEndpoints; + + delegate.onReportEnd = ^{ + XCTAssertNotNil(dataVersionForPartsList); + XCTAssertNotNil(testClusterDataValueForPartsList); + testEndpoints = [self getEndpointArrayFromPartsList:testDataForPartsList forDevice:device]; + + // Make sure that the cluster data in the data storage is populated with cluster index and cluster data for endpoints 0, 1 and 2. + // We do not need to check _persistedClusterData here. _persistedClusterData will be paged in from storage when needed so + // just checking data storage should suffice here. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + + // Populate the initialClusterIndex to use as a reference for all cluster paths later. + for (NSNumber * endpoint in testEndpoints) { + [initialClusterIndex setObject:[controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:endpoint] forKey:endpoint]; + } + + for (NSNumber * endpoint in testEndpoints) { + for (NSNumber * cluster in [initialClusterIndex objectForKey:endpoint]) { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + } + } + }); + [subscriptionExpectation fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ subscriptionExpectation ] timeout:60]; + + // Inject a fake attribute report deleting endpoint 2 from the parts list at the root endpoint. + dataVersionForPartsList = [NSNumber numberWithUnsignedLongLong:(dataVersionForPartsList.unsignedLongLongValue + 1)]; + + // Delete endpoint 2 from the attribute value in parts list. + NSNumber * toBeDeletedEndpoint = @2; + id endpointData = + @{ + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : toBeDeletedEndpoint, + } + }; + + [testClusterDataValueForPartsList removeObject:endpointData]; + + NSArray *> * attributeReport = @[ @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:rootEndpoint clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)], + MTRDataKey : @ { + MTRDataVersionKey : dataVersionForPartsList, + MTRTypeKey : MTRArrayValueType, + MTRValueKey : testClusterDataValueForPartsList, + } + } ]; + + XCTestExpectation * attributeDataReceivedExpectation = [self expectationWithDescription:@"Injected Attribute data received"]; + XCTestExpectation * reportEndExpectation = [self expectationWithDescription:@"Injected Attribute data report ended"]; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + XCTAssertGreaterThan(attributeReport.count, 0); + + for (NSDictionary * attributeDict in attributeReport) { + MTRAttributePath * attributePath = attributeDict[MTRAttributePathKey]; + XCTAssertNotNil(attributePath); + + // Get the new updated parts list value to get the new test endpoints. + if ([attributePath.endpoint isEqualToNumber:rootEndpoint] && attributePath.cluster.unsignedLongValue == MTRClusterIDTypeDescriptorID && attributePath.attribute.unsignedLongValue == MTRAttributeIDTypeClusterDescriptorAttributePartsListID) { + testDataForPartsList = attributeDict[MTRDataKey]; + XCTAssertNotNil(testDataForPartsList); + id dataValue = testDataForPartsList[MTRValueKey]; + XCTAssertNotNil(dataValue); + testClusterDataValueForPartsList = [dataValue mutableCopy]; + } + } + [attributeDataReceivedExpectation fulfill]; + }; + + delegate.onReportEnd = ^{ + XCTAssertNotNil(testClusterDataValueForPartsList); + testEndpoints = [self getEndpointArrayFromPartsList:testDataForPartsList forDevice:device]; + + // Make sure that the cluster data in the data storage for endpoints 0 and 1 are present but not for endpoint 2. + // We do not need to check _persistedClusterData here. _persistedClusterData will be paged in from storage when needed so + // just checking data storage should suffice here. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) { + XCTAssertNotNil(initialClusterIndex); + for (NSNumber * cluster in [initialClusterIndex objectForKey:endpoint]) { + if ([endpoint isEqualToNumber:toBeDeletedEndpoint]) { + XCTAssertNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + } else { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + } + } + } + }); + [reportEndExpectation fulfill]; + }; + + [device unitTestInjectAttributeReport:attributeReport fromSubscription:YES]; + + [self waitForExpectations:@[ attributeDataReceivedExpectation, reportEndExpectation ] timeout:60]; + + [controller.controllerDataStore clearAllStoredClusterData]; + NSDictionary * storedClusterDataAfterClear = [controller.controllerDataStore getStoredClusterDataForNodeID:deviceID]; + XCTAssertEqual(storedClusterDataAfterClear.count, 0); + + [controller removeDevice:device]; + // Reset our commissionee. + __auto_type * baseDevice = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller]; + ResetCommissionee(baseDevice, queue, self, kTimeoutInSeconds); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + +- (void)testDataStorageUpdatesWhenRemovingClusters +{ + NSNumber * deviceID = @(17); + __auto_type * device = [self getMTRDevice:deviceID]; + __auto_type queue = dispatch_get_main_queue(); + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + __auto_type * controller = device.deviceController; + + XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"]; + + __block NSNumber * dataVersionForServerList; + __block NSNumber * testEndpoint = @1; + + // This test will do the following - + // 1. Get the data version and attribute value of the server list for endpoint 1 to inject a fake report. The attribute report will delete cluster ID - MTRClusterIDTypeIdentifyID. + // That should cause the cluster to be removed from cluster index for endpoint 1 and the cluster data for the removed cluster should be cleared from data storage. + // 2. The data store is populated with MTRClusterIDTypeIdentifyID in the cluster index and cluster data for endpoint 1 initially. + // 3. After the fake attribute report is injected with deleted cluster ID - MTRClusterIDTypeIdentifyID, make sure the data store is still populated with cluster index and + // cluster data for all other clusters at endpoint 1 but not the deleted cluster. + __block id testClusterDataValue; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + XCTAssertGreaterThan(attributeReport.count, 0); + + for (NSDictionary * attributeDict in attributeReport) { + MTRAttributePath * attributePath = attributeDict[MTRAttributePathKey]; + XCTAssertNotNil(attributePath); + + if ([attributePath.endpoint isEqualToNumber:testEndpoint] && attributePath.cluster.unsignedLongValue == MTRClusterIDTypeDescriptorID && attributePath.attribute.unsignedLongValue == MTRAttributeIDTypeClusterDescriptorAttributeServerListID) { + MTRDeviceDataValueDictionary data = attributeDict[MTRDataKey]; + XCTAssertNotNil(data); + dataVersionForServerList = data[MTRDataVersionKey]; + id dataValue = data[MTRValueKey]; + XCTAssertNotNil(dataValue); + testClusterDataValue = [dataValue mutableCopy]; + } + } + }; + + __block NSMutableArray * initialClusterIndex = [[NSMutableArray alloc] init]; + __block NSNumber * toBeDeletedCluster = @(MTRClusterIDTypeIdentifyID); + + delegate.onReportEnd = ^{ + XCTAssertNotNil(dataVersionForServerList); + XCTAssertNotNil(testClusterDataValue); + + // Make sure that the cluster data in the data storage has cluster ID - MTRClusterIDTypeIdentifyID in the cluster index for endpoint 1 + // and cluster data for MTRClusterIDTypeIdentifyID exists. + // We do not need to check _persistedClusterData here. _persistedClusterData will be paged in from storage when needed so + // just checking data storage should suffice here. + dispatch_sync(self->_storageQueue, ^{ + initialClusterIndex = [[controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:testEndpoint] mutableCopy]; + XCTAssertTrue([initialClusterIndex containsObject:toBeDeletedCluster]); + for (NSNumber * cluster in initialClusterIndex) { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:testEndpoint clusterID:cluster]); + } + }); + [subscriptionExpectation fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ subscriptionExpectation ] timeout:60]; + + // Inject a fake attribute report after removing cluster ID - MTRClusterIDTypeIdentifyID from endpoint 1 to the server list. + dataVersionForServerList = [NSNumber numberWithUnsignedLongLong:(dataVersionForServerList.unsignedLongLongValue + 1)]; + id identifyClusterData = + @{ + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : toBeDeletedCluster, + } + }; + [testClusterDataValue removeObject:identifyClusterData]; + + NSArray *> * attributeReport = @[ @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:testEndpoint clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributeServerListID)], + MTRDataKey : @ { + MTRDataVersionKey : dataVersionForServerList, + MTRTypeKey : MTRArrayValueType, + MTRValueKey : testClusterDataValue, + } + } ]; + + XCTestExpectation * attributeDataReceivedExpectation = [self expectationWithDescription:@"Injected Attribute data received"]; + XCTestExpectation * reportEndExpectation = [self expectationWithDescription:@"Injected Attribute data report ended"]; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + XCTAssertGreaterThan(attributeReport.count, 0); + [attributeDataReceivedExpectation fulfill]; + }; + + delegate.onReportEnd = ^{ + // Make sure that the cluster data does not have cluster ID - MTRClusterIDTypeIdentifyID in the cluster index for endpoint 1 + // and cluster data for MTRClusterIDTypeIdentifyID is nil. + // We do not need to check _persistedClusterData here. _persistedClusterData will be paged in from storage when needed so + // just checking data storage should suffice here. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertFalse([[controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:testEndpoint] containsObject:toBeDeletedCluster]); + for (NSNumber * cluster in initialClusterIndex) { + if ([cluster isEqualToNumber:toBeDeletedCluster]) { + XCTAssertNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:testEndpoint clusterID:cluster]); + } else { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:testEndpoint clusterID:cluster]); + } + } + }); + [reportEndExpectation fulfill]; + }; + + [device unitTestInjectAttributeReport:attributeReport fromSubscription:YES]; + + [self waitForExpectations:@[ attributeDataReceivedExpectation, reportEndExpectation ] timeout:60]; + + [controller.controllerDataStore clearAllStoredClusterData]; + NSDictionary * storedClusterDataAfterClear = [controller.controllerDataStore getStoredClusterDataForNodeID:deviceID]; + XCTAssertEqual(storedClusterDataAfterClear.count, 0); + + [controller removeDevice:device]; + // Reset our commissionee. + __auto_type * baseDevice = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller]; + ResetCommissionee(baseDevice, queue, self, kTimeoutInSeconds); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + +- (void)testDataStorageUpdatesWhenRemovingAttributes +{ + NSNumber * deviceID = @(17); + __auto_type * device = [self getMTRDevice:deviceID]; + __auto_type queue = dispatch_get_main_queue(); + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + __auto_type * controller = device.deviceController; + + XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"]; + + __block NSNumber * dataVersionForIdentify; + __block NSNumber * testEndpoint = @(1); + __block NSNumber * toBeDeletedAttribute = @(1); + __block id testClusterDataValue; + + // This test will do the following - + // 1. Get the data version and attribute value of the attribute list for endpoint 1 to inject a fake report with attribute 1 removed from MTRClusterIDTypeIdentifyID. + // 2. The data store is populated with cluster data for MTRClusterIDTypeIdentifyID cluster and has all attributes including attribute 1. + // 3. After the fake attribute report is injected, make sure the data store is populated with cluster data for all attributes in MTRClusterIDTypeIdentifyID + // cluster except for attribute 1 which has been deleted. + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + XCTAssertGreaterThan(attributeReport.count, 0); + + for (NSDictionary * attributeDict in attributeReport) { + MTRAttributePath * attributePath = attributeDict[MTRAttributePathKey]; + XCTAssertNotNil(attributePath); + + if ([attributePath.endpoint isEqualToNumber:testEndpoint] && attributePath.cluster.unsignedLongValue == MTRClusterIDTypeIdentifyID && attributePath.attribute.unsignedLongValue == MTRAttributeIDTypeGlobalAttributeAttributeListID) { + MTRDeviceDataValueDictionary data = attributeDict[MTRDataKey]; + XCTAssertNotNil(data); + dataVersionForIdentify = data[MTRDataVersionKey]; + id dataValue = data[MTRValueKey]; + XCTAssertNotNil(dataValue); + testClusterDataValue = [dataValue mutableCopy]; + } + } + }; + + __block NSMutableArray * initialTestAttributes = [[NSMutableArray alloc] init]; + + delegate.onReportEnd = ^{ + XCTAssertNotNil(dataVersionForIdentify); + XCTAssertNotNil(testClusterDataValue); + + dispatch_sync(self->_storageQueue, ^{ + for (NSNumber * cluster in [controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:testEndpoint]) { + + // Make sure that the cluster data in the data storage is populated with cluster data for MTRClusterIDTypeIdentifyID cluster + // and has all attributes including attribute 1. + // We will page in the cluster data from storage to check the above. + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:testEndpoint clusterID:cluster]; + + if ([cluster isEqualToNumber:@(MTRClusterIDTypeIdentifyID)]) { + MTRDeviceClusterData * data = [device _getClusterDataForPath:path]; + XCTAssertNotNil(data); + XCTAssertNotNil(data.attributes); + + MTRDeviceDataValueDictionary dict = [data.attributes objectForKey:@(MTRAttributeIDTypeGlobalAttributeAttributeListID)]; + XCTAssertNotNil(dict); + + NSMutableArray * persistedAttributes = [device arrayOfNumbersFromAttributeValue:dict]; + initialTestAttributes = [device arrayOfNumbersFromAttributeValue:@ { MTRTypeKey : MTRArrayValueType, MTRValueKey : testClusterDataValue }]; + XCTAssertNotNil(persistedAttributes); + for (NSNumber * attribute in initialTestAttributes) { + XCTAssertTrue([persistedAttributes containsObject:attribute]); + } + } + } + }); + + [subscriptionExpectation fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ subscriptionExpectation ] timeout:60]; + + dataVersionForIdentify = [NSNumber numberWithUnsignedLongLong:(dataVersionForIdentify.unsignedLongLongValue + 1)]; + id attributeData = + @{ + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : toBeDeletedAttribute, + } + }; + + [testClusterDataValue removeObject:attributeData]; + + NSArray *> * attributeReport = @[ @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:testEndpoint clusterID:@(MTRClusterIDTypeIdentifyID) attributeID:@(MTRAttributeIDTypeGlobalAttributeAttributeListID)], + MTRDataKey : @ { + MTRDataVersionKey : dataVersionForIdentify, + MTRTypeKey : MTRArrayValueType, + MTRValueKey : testClusterDataValue, + } + } ]; + + XCTestExpectation * attributeDataReceivedExpectation = [self expectationWithDescription:@"Injected Attribute data received"]; + XCTestExpectation * reportEndExpectation = [self expectationWithDescription:@"Injected Attribute data report ended"]; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + XCTAssertGreaterThan(attributeReport.count, 0); + + [attributeDataReceivedExpectation fulfill]; + }; + + delegate.onReportEnd = ^{ + // Make sure that the cluster data in the data storage is populated with cluster data for MTRClusterIDTypeIdentifyID cluster + // and has all attributes except attribute 1 which was deleted. + // We will page in the cluster data from storage to check the above. + dispatch_sync(self->_storageQueue, ^{ + for (NSNumber * cluster in [controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:testEndpoint]) { + MTRDeviceClusterData * clusterData = [controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:testEndpoint clusterID:cluster]; + XCTAssertNotNil(clusterData); + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:testEndpoint clusterID:cluster]; + + if ([cluster isEqualToNumber:@(MTRClusterIDTypeIdentifyID)]) { + MTRDeviceClusterData * data = [device _getClusterDataForPath:path]; + XCTAssertNotNil(data); + XCTAssertNotNil(data.attributes); + + MTRDeviceDataValueDictionary attributeListValue = [data.attributes objectForKey:@(MTRAttributeIDTypeGlobalAttributeAttributeListID)]; + XCTAssertNotNil(attributeListValue); + + NSMutableArray * persistedAttributes = [device arrayOfNumbersFromAttributeValue:attributeListValue]; + XCTAssertNotNil(persistedAttributes); + for (NSNumber * attribute in initialTestAttributes) { + if ([attribute isEqualToNumber:toBeDeletedAttribute]) { + XCTAssertFalse([persistedAttributes containsObject:attribute]); + } else { + XCTAssertTrue([persistedAttributes containsObject:attribute]); + } + } + } + } + }); + + [reportEndExpectation fulfill]; + }; + + [device unitTestInjectAttributeReport:attributeReport fromSubscription:YES]; + + [self waitForExpectations:@[ attributeDataReceivedExpectation, reportEndExpectation ] timeout:60]; + + [controller.controllerDataStore clearAllStoredClusterData]; + NSDictionary * storedClusterDataAfterClear = [controller.controllerDataStore getStoredClusterDataForNodeID:deviceID]; + XCTAssertEqual(storedClusterDataAfterClear.count, 0); + + [controller removeDevice:device]; + // Reset our commissionee. + __auto_type * baseDevice = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller]; + ResetCommissionee(baseDevice, queue, self, kTimeoutInSeconds); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + @end diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h index 5c55685d6db741..c18daf6199d8e3 100644 --- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h +++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h @@ -33,6 +33,9 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)_endpointIndexKeyForNodeID:(NSNumber *)nodeID; - (NSString *)_clusterIndexKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID; - (NSString *)_clusterDataKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID; +- (nullable NSArray *)_fetchEndpointIndexForNodeID:(NSNumber *)nodeID; +- (nullable NSArray *)_fetchClusterIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID; +- (nullable MTRDeviceClusterData *)_fetchClusterDataForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID; @end // Declare internal methods for testing @@ -44,6 +47,9 @@ NS_ASSUME_NONNULL_BEGIN @interface MTRDevice (Test) - (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary *)theOther; +- (MTRDeviceClusterData *)_getClusterDataForPath:(MTRClusterPath *)path; +- (BOOL)_clusterHasBeenPersisted:(MTRClusterPath *)path; +- (NSMutableArray *)arrayOfNumbersFromAttributeValue:(MTRDeviceDataValueDictionary)dataDictionary; @end #pragma mark - Declarations for items compiled only for DEBUG configuration