diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 478485d509ccd1..2833f38d5f8bbc 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -196,6 +196,11 @@ - (void)storeValue:(MTRDeviceDataValueDictionary _Nullable)value forAttribute:(N _attributes[attribute] = value; } +- (void)_removeValueForAttribute:(NSNumber *)attribute +{ + [_attributes removeObjectForKey:attribute]; +} + - (NSDictionary *)attributes { return _attributes; @@ -1374,6 +1379,18 @@ - (void)_setCachedAttributeValue:(MTRDeviceDataValueDictionary _Nullable)value f _clusterDataToPersist[clusterPath] = clusterData; } +- (void)_removeCachedAttributeValue:(MTRClusterPath *)clusterPath forPath:(MTRAttributePath *)attributePath +{ + os_unfair_lock_assert_owner(&self->_lock); + + if (_clusterDataToPersist == nil) { + return; + } + auto * clusterData = [_clusterDataToPersist objectForKey:clusterPath]; + [clusterData _removeValueForAttribute:attributePath.attribute]; + [_clusterDataToPersist setObject:clusterData forKey:clusterPath]; +} + - (void)_createDataVersionFilterListFromDictionary:(NSDictionary *)dataVersions dataVersionFilterList:(DataVersionFilter **)dataVersionFilterList count:(size_t *)count sizeReduction:(size_t)sizeReduction { size_t maxDataVersionFilterSize = dataVersions.count; @@ -2431,7 +2448,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]) { @@ -2493,6 +2510,137 @@ - (BOOL)_attributeAffectsDeviceConfiguration:(MTRAttributePath *)attributePath return NO; } +- (BOOL)_needsPruningOfEndpointsAndClusters:(MTRAttributePath *)attributePath +{ + // Check for attributes in the descriptor cluster that could cause removal of endpoints and clusters. + if (attributePath.cluster.unsignedLongValue == MTRClusterIDTypeDescriptorID) { + switch (attributePath.attribute.unsignedLongValue) { + case MTRAttributeIDTypeClusterDescriptorAttributePartsListID: + case MTRAttributeIDTypeClusterDescriptorAttributeServerListID: + return YES; + } + } + + // Check for global attribute - attribute list that could cause removal of attributes. + switch (attributePath.attribute.unsignedLongValue) { + case MTRAttributeIDTypeGlobalAttributeAttributeListID: + return YES; + } + return NO; +} + +- (void)_pruneOrphanedEndpointsAndClusters:(MTRAttributePath *)attributePath + previousValue:(NSDictionary *)previousValue + attributeDataValue:(NSDictionary *)attributeDataValue +{ + os_unfair_lock_assert_owner(&self->_lock); + + if (_persistedClusters == nil || _persistedClusterData == nil || !previousValue.count) + { + 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) { + switch (attributePath.attribute.unsignedLongValue) { + + // If the parts list changed and one or more endpoints were removed, remove all the clusters in _persistedClusters and _persistedClusterData for all those endpoints. + // Also remove it from the data store. + case MTRAttributeIDTypeClusterDescriptorAttributePartsListID: + { + NSMutableSet * toBeRemovedEndpoints = [NSMutableSet setWithArray:[self arrayOfNumbersFromAttributeValue:previousValue]]; + NSSet * endpointsOnDevice = [NSSet setWithArray:[self arrayOfNumbersFromAttributeValue:attributeDataValue]]; + [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]; + [_persistedClusterData removeObjectForKey:path]; + [self.deviceController.controllerDataStore clearStoredClusterDataForNodeIDWithEndpointID:self.nodeID endpointID:endpoint]; + } + } + [_persistedClusters minusSet:clusterPathsToRemove]; + } + break; + } + + // If the server list changed and clusters were removed, remove the clusters from the _persistedClusters and _persistedClusterData for that endpoint + // Also remove it from the data store. + case MTRAttributeIDTypeClusterDescriptorAttributeServerListID: + { + NSMutableSet * toBeRemovedClusters= [NSMutableSet setWithArray:[self arrayOfNumbersFromAttributeValue:previousValue]]; + NSSet * clustersOnDevice = [NSSet setWithArray:[self arrayOfNumbersFromAttributeValue:attributeDataValue]]; + [toBeRemovedClusters minusSet:clustersOnDevice]; + + NSMutableSet * clusterPathsToRemove = [[NSMutableSet alloc]init]; + for (NSNumber * cluster in toBeRemovedClusters) + { + for (MTRClusterPath * path in _persistedClusters) + { + if ([path.endpoint isEqualToNumber:attributePath.endpoint] && [path.cluster isEqualToNumber:cluster]) + { + [clusterPathsToRemove addObject:path]; + [_persistedClusterData removeObjectForKey:path]; + + [self.deviceController.controllerDataStore clearStoredClusterDataForNodeIDWithClusterID:self.nodeID endpointID:path.endpoint clusterID:path.cluster]; + } + } + } + [_persistedClusters minusSet:clusterPathsToRemove]; + break; + } + } + } + + switch (attributePath.attribute.unsignedLongValue) { + // If the attribute list changed and attributes were removed, remove the attributes from the _persistedClusterData for that cluster and endpoint. + // Also remove it from the data store cluster data. + case MTRAttributeIDTypeGlobalAttributeAttributeListID: + { + NSMutableSet * toBeRemovedAttributes= [NSMutableSet setWithArray:[self arrayOfNumbersFromAttributeValue:[self _cachedAttributeValueForPath:attributePath]]]; + NSSet * attributesOnDevice = [NSSet setWithArray:[self arrayOfNumbersFromAttributeValue:attributeDataValue]]; + + [toBeRemovedAttributes minusSet:attributesOnDevice]; + for (NSNumber * attribute in toBeRemovedAttributes) + { + for (MTRClusterPath * path in _persistedClusters) + { + if ([path.endpoint isEqualToNumber:attributePath.endpoint] && [path.cluster isEqualToNumber:attributePath.cluster]) + { + MTRDeviceClusterData * clusterData = [self _clusterDataForPath:path]; + if (clusterData == nil) + { + return; + } + [clusterData _removeValueForAttribute:attribute]; + [self->_persistedClusterData setObject:clusterData forKey:path]; + + NSDictionary * dataStoreClusterData = [self.deviceController.controllerDataStore getStoredClusterDataForNodeID:self.nodeID]; + NSMutableDictionary * dataStoreClusterDataCopy = [dataStoreClusterData mutableCopy]; + for (MTRClusterPath * dataStorePath in dataStoreClusterData) + { + if ([dataStorePath isEqualTo:path]) + { + [dataStoreClusterDataCopy removeObjectForKey:path]; + [dataStoreClusterDataCopy setObject:clusterData forKey:path]; + [self.deviceController.controllerDataStore storeClusterData:dataStoreClusterDataCopy forNodeID:self.nodeID]; + dataStoreClusterData = [NSMutableDictionary dictionaryWithDictionary:[self.deviceController.controllerDataStore getStoredClusterDataForNodeID:self.nodeID]]; + } + } + [self _removeCachedAttributeValue:path forPath:attributePath]; + } + } + } + break; + } + } +} + // assume lock is held - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray *> *)reportedAttributeValues { @@ -2544,13 +2692,19 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray_lock); + if (_persistedClusters == nil || _persistedClusterData == nil) + { + return; + } + + [_persistedClusterData removeObjectForKey:path]; + [_persistedClusters removeObject:path]; +} + +- (NSMutableSet *)_getPersistedClusters +{ + std::lock_guard lock(_lock); + + return _persistedClusters; +} + +- (MTRDeviceClusterData *)_getPersistedClusterDataForPath:(MTRClusterPath *)path +{ + std::lock_guard lock(_lock); + + if ([_persistedClusters containsObject:path]) + { + return [_persistedClusterData objectForKey:path]; + } + return nil; +} + +- (BOOL)_persistedClusterContains:(MTRClusterPath *)path +{ + std::lock_guard lock(_lock); + + if ([_persistedClusters containsObject:path]) + { + return YES; + } + return NO; +} + - (BOOL)deviceCachePrimed { std::lock_guard lock(_lock); diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h index dd9da5c03eccff..070619362ea781 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h @@ -76,6 +76,8 @@ typedef void (^MTRDeviceControllerDataStoreClusterDataHandler)(NSDictionary *)clusterData forNodeID:(NSNumber *)nodeID; - (void)clearStoredClusterDataForNodeID:(NSNumber *)nodeID; +- (void)clearStoredClusterDataForNodeIDWithEndpointID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID; +- (void)clearStoredClusterDataForNodeIDWithClusterID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID; - (void)clearAllStoredClusterData; @end diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm index 1f515f25565ca5..dbfa357f5af558 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm @@ -446,6 +446,25 @@ - (BOOL)_storeEndpointIndex:(NSArray *)endpointIndex forNodeID:(NSNu return [self _storeAttributeCacheValue:endpointIndex forKey:[self _endpointIndexKeyForNodeID:nodeID]]; } +- (BOOL)_deleteEndpointIndex:(NSNumber *)endpointID forNodeID:(NSNumber *)nodeID +{ + dispatch_assert_queue(_storageDelegateQueue); + + if (!endpointID || !nodeID) { + MTR_LOG_ERROR("%s: unexpected nil input", __func__); + return NO; + } + + NSMutableArray * endpointIndex = [NSMutableArray arrayWithArray:[self _fetchEndpointIndexForNodeID:nodeID]]; + if (endpointIndex == nil) + { + return NO; + } + + [endpointIndex removeObject:endpointID]; + return [self _storeAttributeCacheValue:endpointIndex forKey:[self _endpointIndexKeyForNodeID:nodeID]]; +} + - (BOOL)_deleteEndpointIndexForNodeID:(NSNumber *)nodeID { dispatch_assert_queue(_storageDelegateQueue); @@ -497,7 +516,6 @@ - (BOOL)_deleteClusterIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)e MTR_LOG_ERROR("%s: unexpected nil input", __func__); return NO; } - return [self _removeAttributeCacheValueForKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]]; } @@ -540,8 +558,8 @@ - (BOOL)_deleteClusterDataForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)en MTR_LOG_ERROR("%s: unexpected nil input", __func__); return NO; } - - return [self _removeAttributeCacheValueForKey:[self _clusterDataKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]]; + BOOL value = [self _removeAttributeCacheValueForKey:[self _clusterDataKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]]; + return value; } #pragma - Attribute Cache management @@ -699,6 +717,65 @@ - (void)clearStoredClusterDataForNodeID:(NSNumber *)nodeID }); } +- (BOOL)_clearStoredClusterDataForNodeIDWithEndpointID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID +{ + dispatch_assert_queue(_storageDelegateQueue); + NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID]; + + BOOL success = NO; + for (NSNumber * clusterID in clusterIndex) { + success = [self _deleteClusterDataForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + if (!success) { + MTR_LOG_INFO("_clearStoredClusterDataForNodeIDWithEndpointID: Delete failed for clusterData @ node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue); + } + } + + success = [self _deleteClusterIndexForNodeID:nodeID endpointID:endpointID]; + if (!success) { + MTR_LOG_INFO("Delete failed for clusterIndex @ node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); + } + + success = [self _deleteEndpointIndex:endpointID forNodeID:(nodeID)]; + if (!success) { + MTR_LOG_INFO("Delete failed for endpoint index %@ for @ node 0x%016llX", endpointID, nodeID.unsignedLongLongValue); + } + return success; +} + +- (void)clearStoredClusterDataForNodeIDWithEndpointID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID +{ + dispatch_async(_storageDelegateQueue, ^{ + BOOL success = [self _clearStoredClusterDataForNodeIDWithEndpointID:nodeID endpointID:endpointID]; + if (!success) { + MTR_LOG_INFO("clearStoredClusterDataForNodeIDWithEndpointID: Delete failed for clusterData for @ node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); + } + MTR_LOG_INFO("clearStoredClusterDataForNodeIDWithEndpointID: Successfully deleted cluster index and data for @ node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); + }); +} + +- (void)clearStoredClusterDataForNodeIDWithClusterID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID +{ + dispatch_async(_storageDelegateQueue, ^{ + BOOL success = [self _deleteClusterDataForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + if (!success) { + MTR_LOG_INFO("clearStoredClusterDataForNodeIDWithClusterID: _deleteClusterDataForNodeID failed for @ node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue); + return; + } + + NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID]; + NSMutableArray * clusterIndexCopy = [clusterIndex mutableCopy]; + [clusterIndexCopy removeObject:clusterID]; + + if (clusterIndexCopy.count != clusterIndex.count) { + success = [self _storeClusterIndex:clusterIndexCopy forNodeID:nodeID endpointID:endpointID]; + if (!success) { + MTR_LOG_INFO("clearStoredClusterDataForNodeIDWithClusterID: _storeClusterIndex failed for @ node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); + } + } + MTR_LOG_INFO("clearStoredClusterDataForNodeIDWithClusterID: Deleted endpoint %u cluster 0x%08lX for @ node 0x%016llX successfully", endpointID.unsignedShortValue, clusterID.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 7302a5b11d024c..7455a21b1a9ba8 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDevice_Internal.h @@ -39,6 +39,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 06968b00292ef4..67b795af007175 100644 --- a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m +++ b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m @@ -2004,4 +2004,981 @@ - (void)testControllerServer [controllerServer shutdown]; } +- (void)test012_TestDataStoreMTRDeviceDoNotPruneOrphanedEndpoints +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type queue = dispatch_get_main_queue(); + + __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); + + // Now commission the device, to test that that works. + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + __auto_type * device = [MTRDevice deviceWithNodeID:deviceID controller:controller]; + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + + XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"]; + + __block unsigned attributeReportsReceived = 0; + __block NSNumber * dataVersionForPartsList; + __block NSNumber * rootEndpoint = @0; + __block NSNumber * notToBeDeletedEndpoint1 = @1; + __block NSNumber * notToBeDeletedEndpoint2 = @2; + __block NSNumber * toBeAddedEndpoint = @3; + + // 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 with an added endpoint so we do + // not prune any endpoints or clusters. + // 2. The data store is populated with cluster index and cluster data for endpoints 0, 1 and 2 initially. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for each endpoint. + // 3. After the fake attribute report is injected, make sure the data store is still populated with cluster index and cluster data for endpoints 0, 1 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index. + __block NSMutableArray * testClusterDataValueForPartsList; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 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) { + NSDictionary * data = attributeDict[MTRDataKey]; + XCTAssertNotNil(data); + dataVersionForPartsList = data[MTRDataVersionKey]; + id dataValue = data[MTRValueKey]; + XCTAssertNotNil(dataValue); + testClusterDataValueForPartsList = [dataValue mutableCopy]; + } + } + }; + + __block NSMutableArray *> * initialClusterIndex = [[NSMutableArray alloc]init]; + + // TODO: Build the test endpoints from the attribute data value + __block NSArray * testEndpoints = @[ rootEndpoint, notToBeDeletedEndpoint1, notToBeDeletedEndpoint2]; + delegate.onReportEnd = ^{ + XCTAssertNotNil(dataVersionForPartsList); + XCTAssertNotNil(testClusterDataValueForPartsList); + + // Verify that the data store is populated with cluster index and cluster data for endpoints 0, 1 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + [initialClusterIndex insertObject:[controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:endpoint] atIndex:endpoint.unsignedShortValue]; + XCTAssertNotNil(initialClusterIndex); + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + } + } + }); + [subscriptionExpectation fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ subscriptionExpectation ] timeout:60]; + + // Inject a fake attribute report adding endpoint 3 to the parts list at endpoint 0. + dataVersionForPartsList = [NSNumber numberWithUnsignedLongLong:(dataVersionForPartsList.unsignedLongLongValue + 1)]; + [testClusterDataValueForPartsList addObject:@{ + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : toBeAddedEndpoint, + } + }]; + + 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) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 0); + [attributeDataReceivedExpectation fulfill]; + }; + + delegate.onReportEnd = ^{ + + // Verify that the data store is still populated with cluster index and cluster data for endpoints 0, 1 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + XCTAssertNotNil(initialClusterIndex); + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + } + } + }); + [reportEndExpectation fulfill]; + }; + + [device unitTestInjectAttributeReport:attributeReport]; + + [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)test013_TestDataStoreMTRDevicePruneOrphanedEndpoints +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type queue = dispatch_get_main_queue(); + + __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); + + // Now commission the device, to test that that works. + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + __auto_type * device = [MTRDevice deviceWithNodeID:deviceID controller:controller]; + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + + XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"]; + + __block unsigned attributeReportsReceived = 0; + __block NSNumber * dataVersionForPartsList; + __block NSNumber * rootEndpoint = @0; + __block NSNumber * notToBeDeletedEndpoint = @1; + __block NSNumber * toBeDeletedEndpoint = @2; + + // 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 with a removed endpoint (endpoint 2) so we + // prune endpoint 2 and all its clusters. + // 2. The data store is populated with cluster index and cluster data for endpoints 0, 1 and 2 initially. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for each endpoint. + // 3. After the fake attribute report is injected, make sure the data store is still populated with cluster index and cluster data for endpoints 0 and 1 but not for endpoint 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for endpoints 0 and 1 but not for endpoint 2. + __block NSMutableArray * testClusterDataValueForPartsList; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 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) { + NSDictionary * data = attributeDict[MTRDataKey]; + XCTAssertNotNil(data); + dataVersionForPartsList = data[MTRDataVersionKey]; + id dataValue = data[MTRValueKey]; + XCTAssertNotNil(dataValue); + testClusterDataValueForPartsList = [dataValue mutableCopy]; + } + } + }; + + __block NSMutableArray *> * initialClusterIndex = [[NSMutableArray alloc]init]; + + // TODO: Build the test endpoints from the attribute data value + __block NSArray * testEndpoints = @[ rootEndpoint, notToBeDeletedEndpoint, toBeDeletedEndpoint]; + delegate.onReportEnd = ^{ + XCTAssertNotNil(dataVersionForPartsList); + XCTAssertNotNil(testClusterDataValueForPartsList); + + // Verify that the data store is populated with cluster index and cluster data for endpoints 0, 1 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + [initialClusterIndex insertObject:[controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:endpoint] atIndex:endpoint.unsignedShortValue]; + XCTAssertNotNil(initialClusterIndex); + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + } + } + }); + [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)]; + // TODO: try to delete entries from the actual data received in the initila attribute report to make the neunsignedIntegerArrayValue + NSArray *> * unsignedIntegerArrayValue = @[ + @{ + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : notToBeDeletedEndpoint, + } + }, + ]; + + NSArray *> * attributeReport = @[ @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:rootEndpoint clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)], + MTRDataKey : @ { + MTRDataVersionKey : dataVersionForPartsList, + MTRTypeKey : MTRArrayValueType, + MTRValueKey : unsignedIntegerArrayValue, + } + } ]; + + XCTestExpectation * attributeDataReceivedExpectation = [self expectationWithDescription:@"Injected Attribute data received"]; + XCTestExpectation * reportEndExpectation = [self expectationWithDescription:@"Injected Attribute data report ended"]; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 0); + [attributeDataReceivedExpectation fulfill]; + }; + + delegate.onReportEnd = ^{ + // Verify that the data store is still populated with cluster index and cluster data for endpoints 0 and 1 but not for endpoint 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for endpoints 0 and 1 but not endpoint 2. + NSMutableArray * newTestEndpoints = [testEndpoints mutableCopy]; + [newTestEndpoints removeObject:toBeDeletedEndpoint]; + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:newTestEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + XCTAssertNotNil(initialClusterIndex); + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + if (![endpoint isEqualToNumber:toBeDeletedEndpoint]) + { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + } else + { + XCTAssertNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + XCTAssertFalse([device _persistedClusterContains:path]); + XCTAssertNil([device _getPersistedClusterDataForPath:path]); + } + } + } + }); + [reportEndExpectation fulfill]; + }; + + [device unitTestInjectAttributeReport:attributeReport]; + + [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)test014_TestDataStoreMTRDeviceDoNotPruneOrphanedClusters +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type queue = dispatch_get_main_queue(); + + __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); + + // Now commission the device, to test that that works. + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + __auto_type * device = [MTRDevice deviceWithNodeID:deviceID controller:controller]; + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + + XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"]; + + __block unsigned attributeReportsReceived = 0; + __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 with a cluster added to the server list. + // 2. The data store is populated with cluster index and cluster data for endpoints 0, 1 and 2 initially. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for each endpoint. + // 3. After the fake attribute report is injected, make sure the data store is still populated with cluster index and cluster data for endpoints 0, 1 and 2. Nothing was deleted. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for endpoints 0, 1 and 2. + __block NSMutableArray * testClusterDataValue; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 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) { + NSDictionary * data = attributeDict[MTRDataKey]; + XCTAssertNotNil(data); + dataVersionForServerList = data[MTRDataVersionKey]; + id dataValue = data[MTRValueKey]; + XCTAssertNotNil(dataValue); + testClusterDataValue = [dataValue mutableCopy]; + } + } + }; + + __block NSMutableArray *> * initialClusterIndex = [[NSMutableArray alloc]init]; + + // TODO: Build the test endpoints from the attribute data value + __block NSArray * testEndpoints = @[ @0, testEndpoint, @2]; + delegate.onReportEnd = ^{ + XCTAssertNotNil(dataVersionForServerList); + XCTAssertNotNil(testClusterDataValue); + + // Verify that the data store is populated with cluster index and cluster data for endpoints 0, 1 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + [initialClusterIndex insertObject:[controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:endpoint] atIndex:endpoint.unsignedShortValue]; + XCTAssertNotNil(initialClusterIndex); + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + } + } + }); + [subscriptionExpectation fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ subscriptionExpectation ] timeout:60]; + + // Inject a fake attribute report adding a cluster to endpoint 1 to the server list. + dataVersionForServerList = [NSNumber numberWithUnsignedLongLong:(dataVersionForServerList.unsignedLongLongValue + 1)]; + [testClusterDataValue addObject: @{ + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(5), + } + }]; + + 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) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 0); + [attributeDataReceivedExpectation fulfill]; + }; + + delegate.onReportEnd = ^{ + // Verify that the data store is populated with cluster index and cluster data for endpoints 0, 1 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index. Nothing is deleted. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + XCTAssertNotNil(initialClusterIndex); + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + } + } + }); + + [reportEndExpectation fulfill]; + }; + + [device unitTestInjectAttributeReport:attributeReport]; + + [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)test015_TestDataStoreMTRDevicePruneOrphanedClusters +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type queue = dispatch_get_main_queue(); + + __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); + + // Now commission the device, to test that that works. + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + __auto_type * device = [MTRDevice deviceWithNodeID:deviceID controller:controller]; + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + + XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"]; + + __block unsigned attributeReportsReceived = 0; + __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 0 to inject a fake report with all clusters removed except the descriptor cluster. + // 2. The data store is populated with cluster index and cluster data for endpoints 0, 1 and 2 initially. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for each endpoint. + // 3. After the fake attribute report is injected, make sure the data store is still populated with cluster index and cluster data for endpoints 0, 1 and 2. But the cluster index and data for endpoint 1 only has the descriptor cluster. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for endpoints 0 and 2 but only descriptor cluster for endpoint 1. + __block NSArray * testClusterDataValue; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 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) { + NSDictionary * data = attributeDict[MTRDataKey]; + XCTAssertNotNil(data); + dataVersionForServerList = data[MTRDataVersionKey]; + id dataValue = data[MTRValueKey]; + XCTAssertNotNil(dataValue); + testClusterDataValue = [dataValue copy]; + } + } + }; + + __block NSMutableArray *> * initialClusterIndex = [[NSMutableArray alloc]init]; + + // TODO: Build the test endpoints from the attribute data value + __block NSArray * testEndpoints = @[ @0, testEndpoint, @2]; + __block NSNumber * notToBeDeletedClusterID = @29; + delegate.onReportEnd = ^{ + XCTAssertNotNil(dataVersionForServerList); + XCTAssertNotNil(testClusterDataValue); + + // Verify that the data store is populated with cluster index and cluster data for endpoints 0, 1 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + [initialClusterIndex insertObject:[controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:endpoint] atIndex:endpoint.unsignedShortValue]; + XCTAssertNotNil(initialClusterIndex); + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + } + } + }); + [subscriptionExpectation fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ subscriptionExpectation ] timeout:60]; + + // Inject a fake attribute report deleting all clusters except descriptor cluster for endpoint 1 from the server list. + dataVersionForServerList = [NSNumber numberWithUnsignedLongLong:(dataVersionForServerList.unsignedLongLongValue + 1)]; + NSArray *> * unsignedIntegerArrayValue = @[ + @{ + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : notToBeDeletedClusterID, + } + }, + ]; + + NSArray *> * attributeReport = @[ @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:testEndpoint clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributeServerListID)], + MTRDataKey : @ { + MTRDataVersionKey : dataVersionForServerList, + MTRTypeKey : MTRArrayValueType, + MTRValueKey : unsignedIntegerArrayValue, + } + } ]; + + XCTestExpectation * attributeDataReceivedExpectation = [self expectationWithDescription:@"Injected Attribute data received"]; + XCTestExpectation * reportEndExpectation = [self expectationWithDescription:@"Injected Attribute data report ended"]; + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 0); + [attributeDataReceivedExpectation fulfill]; + }; + + delegate.onReportEnd = ^{ + // Verify that the data store is populated with cluster index and cluster data for endpoints 0 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for endpoints 0 and 2. + // For endpoint 1, we should only have 1 entry in the clusterIndex for descriptor cluster. Everything else must be deleted. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + XCTAssertNotNil(initialClusterIndex); + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + if (![endpoint isEqualToNumber:testEndpoint]) + { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + } + else + { + if ([cluster isEqualToNumber:notToBeDeletedClusterID]) + { + XCTAssertNotNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + } + else + { + XCTAssertNil([controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]); + XCTAssertFalse([device _persistedClusterContains:path]); + XCTAssertNil([device _getPersistedClusterDataForPath:path]); + } + } + } + } + }); + + [reportEndExpectation fulfill]; + }; + + [device unitTestInjectAttributeReport:attributeReport]; + + [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)test016_TestDataStoreMTRDevicePruneOrphanedAttributes +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type queue = dispatch_get_main_queue(); + + __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); + + // Now commission the device, to test that that works. + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + __auto_type * device = [MTRDevice deviceWithNodeID:deviceID controller:controller]; + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + + XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"]; + + __block unsigned attributeReportsReceived = 0; + __block NSNumber * dataVersionForIdentify; + __block NSNumber * testEndpoint = @(1); + __block NSNumber * toBeDeletedAttribute = @(1); + __block NSMutableArray * 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. + // 2. The data store is populated with cluster index and cluster data for endpoints 0, 1 and 2 initially. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index for each endpoint. + // 3. After the fake attribute report is injected, make sure the data store is still populated with cluster index and cluster data for endpoints 0, 1 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster data for all clusters in the cluster index for endpoints 0, 1 and 2. + // The cluster data for endpoint 1 for the Identify cluster should have attribute 1 deleted. + delegate.onAttributeDataReceived = ^(NSArray *> * attributeReport) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 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) { + NSDictionary * data = attributeDict[MTRDataKey]; + XCTAssertNotNil(data); + dataVersionForIdentify = data[MTRDataVersionKey]; + id dataValue = data[MTRValueKey]; + XCTAssertNotNil(dataValue); + testClusterDataValue = [dataValue mutableCopy]; + } + } + }; + + __block NSMutableArray *> * initialClusterIndex = [[NSMutableArray alloc]init]; + + // TODO: Build the test endpoints from the attribute data value + __block NSArray * testEndpoints = @[ @0, testEndpoint, @2]; + delegate.onReportEnd = ^{ + XCTAssertNotNil(dataVersionForIdentify); + XCTAssertNotNil(testClusterDataValue); + + // Verify that the data store is populated with cluster index and cluster data for endpoints 0, 1 and 2. + // Also _persistedClusters and _persistedClusterData has the cluster paths for all clusters in the cluster index. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + [initialClusterIndex insertObject:[controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:endpoint] atIndex:endpoint.unsignedShortValue]; + XCTAssertNotNil(initialClusterIndex); + + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + MTRDeviceClusterData * clusterData = [controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]; + XCTAssertNotNil(clusterData); + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + + // For endpoint 1 and the identify cluster make sure the attributes that are present in the persisted cluster and data storage matched the ones received in the attribute report. + if ([endpoint isEqualToNumber:testEndpoint] && [cluster isEqualToNumber:@(MTRClusterIDTypeIdentifyID)]) + { + MTRDeviceClusterData * data = [device _getPersistedClusterDataForPath:path]; + XCTAssertNotNil(data); + XCTAssertNotNil(data.attributes); + + NSDictionary * dict = [data.attributes objectForKey:@(MTRAttributeIDTypeGlobalAttributeAttributeListID)]; + XCTAssertNotNil(dict); + + NSMutableArray * persistedAttributes = [device arrayOfNumbersFromAttributeValue:dict]; + NSMutableArray * testAttributes = [device arrayOfNumbersFromAttributeValue:@ { MTRTypeKey : MTRArrayValueType, MTRValueKey : testClusterDataValue }]; + XCTAssertNotNil(persistedAttributes); + for (NSNumber * attribute in testAttributes) + { + XCTAssertTrue([persistedAttributes containsObject:attribute]); + } + + XCTAssertNotNil(clusterData.attributes); + NSDictionary * dictFromStore = [clusterData.attributes objectForKey:@(MTRAttributeIDTypeGlobalAttributeAttributeListID)]; + XCTAssertNotNil(dictFromStore); + + NSMutableArray * dataStoreAttributes = [device arrayOfNumbersFromAttributeValue:dictFromStore]; + XCTAssertNotNil(dataStoreAttributes); + for (NSNumber * attribute in testAttributes) + { + XCTAssertTrue([dataStoreAttributes containsObject:attribute]); + } + } + } + } + }); + + [subscriptionExpectation fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ subscriptionExpectation ] timeout:60]; + + dataVersionForIdentify = [NSNumber numberWithUnsignedLongLong:(dataVersionForIdentify.unsignedLongLongValue + 1)]; + + [testClusterDataValue removeObjectAtIndex:1]; + + 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) { + attributeReportsReceived += attributeReport.count; + XCTAssert(attributeReportsReceived > 0); + [attributeDataReceivedExpectation fulfill]; + }; + + delegate.onReportEnd = ^{ + // For endpoints 0, 1 and 2, verify that the cluster index exists and the cluster data exists. for endpoint 1, since we deleted attribute 1 from Identify cluster + // we need to make sure that cluster data for attribute 1 is not present in persisted cluster data or data storage. + dispatch_sync(self->_storageQueue, ^{ + XCTAssertTrue([[controller.controllerDataStore _fetchEndpointIndexForNodeID:deviceID] isEqualToArray:testEndpoints]); + for (NSNumber * endpoint in testEndpoints) + { + [initialClusterIndex insertObject:[controller.controllerDataStore _fetchClusterIndexForNodeID:deviceID endpointID:endpoint] atIndex:endpoint.unsignedShortValue]; + XCTAssertNotNil(initialClusterIndex); + + for (NSNumber * cluster in [initialClusterIndex objectAtIndex:endpoint.unsignedShortValue]) + { + MTRDeviceClusterData * clusterData = [controller.controllerDataStore _fetchClusterDataForNodeID:deviceID endpointID:endpoint clusterID:cluster]; + XCTAssertNotNil(clusterData); + MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpoint clusterID:cluster]; + XCTAssertTrue([device _persistedClusterContains:path]); + XCTAssertNotNil([device _getPersistedClusterDataForPath:path]); + + // For endpoint 1, identify cluster make sure the only attribute remaining is attribute 0. + if ([endpoint isEqualToNumber:testEndpoint] && [cluster isEqualToNumber:@(MTRClusterIDTypeIdentifyID)]) + { + MTRDeviceClusterData * data = [device _getPersistedClusterDataForPath:path]; + XCTAssertNotNil(data); + XCTAssertNotNil(data.attributes); + + NSDictionary * dict = [data.attributes objectForKey:@(MTRAttributeIDTypeGlobalAttributeAttributeListID)]; + XCTAssertNotNil(dict); + + NSMutableArray * persistedAttributes = [device arrayOfNumbersFromAttributeValue:dict]; + NSMutableArray * testAttributes = [device arrayOfNumbersFromAttributeValue:@ { MTRTypeKey : MTRArrayValueType, MTRValueKey : testClusterDataValue }]; + XCTAssertNotNil(persistedAttributes); + for (NSNumber * attribute in testAttributes) + { + if ([attribute isEqualToNumber:toBeDeletedAttribute]) + { + XCTAssertFalse([persistedAttributes containsObject:attribute]); + } + else + { + XCTAssertTrue([persistedAttributes containsObject:attribute]); + } + } + + // for the data store make sure the cluster path exists and the cluster data has attribute values + XCTAssertNotNil(clusterData.attributes); + + NSDictionary * dictFromStore = [clusterData.attributes objectForKey:@(MTRAttributeIDTypeGlobalAttributeAttributeListID)]; + XCTAssertNotNil(dictFromStore); + + NSMutableArray * dataStoreAttributes = [device arrayOfNumbersFromAttributeValue:dictFromStore]; + XCTAssertNotNil(dataStoreAttributes); + for (NSNumber * attribute in testAttributes) + { + if ([attribute isEqualToNumber:toBeDeletedAttribute]) + { + XCTAssertFalse([dataStoreAttributes containsObject:attribute]); + } + else + { + XCTAssertTrue([dataStoreAttributes containsObject:attribute]); + } + } + } + } + } + }); + + [reportEndExpectation fulfill]; + }; + + [device unitTestInjectAttributeReport:attributeReport]; + + [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 119822099ce0a3..4ba7335f39ab29 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,10 @@ NS_ASSUME_NONNULL_BEGIN @interface MTRDevice (Test) - (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary *)theOther; +- (MTRDeviceClusterData *)_getPersistedClusterDataForPath:(MTRClusterPath *)path; +- (NSMutableSet *)_getPersistedClusters; +- (BOOL)_persistedClusterContains:(MTRClusterPath *)path; +- (NSMutableArray *)arrayOfNumbersFromAttributeValue:(NSDictionary *)dataDictionary; @end #pragma mark - Declarations for items compiled only for DEBUG configuration