Skip to content

Commit 2f2c4f1

Browse files
authored
[Darwin] MTRDevice attribute cache persistent storage local test facility (#32181)
* [Darwin] MTRDevice attribute cache persistent storage local test facility * Fix header scope * Fix CI compilation issue * Added MTR_PER_CONTROLLER_STORAGE_ENABLED check to fix darwin CI * Fix for the previous fix - now double tested
1 parent c13b324 commit 2f2c4f1

8 files changed

+313
-46
lines changed

src/darwin/Framework/CHIP/MTRBaseDevice.mm

+1-1
Original file line numberDiff line numberDiff line change
@@ -1976,7 +1976,6 @@ - (void)failSubscribers:(dispatch_queue_t)queue completion:(void (^)(void))compl
19761976
MTR_LOG_DEBUG("Causing failure in subscribers on purpose");
19771977
CauseReadClientFailure(self.deviceController, self.nodeID, queue, completion);
19781978
}
1979-
#endif
19801979

19811980
// The following method is for unit testing purpose only
19821981
+ (id)CHIPEncodeAndDecodeNSObject:(id)object
@@ -2018,6 +2017,7 @@ + (id)CHIPEncodeAndDecodeNSObject:(id)object
20182017
}
20192018
return decodedData.GetDecodedObject();
20202019
}
2020+
#endif
20212021

20222022
- (void)readEventsWithEndpointID:(NSNumber * _Nullable)endpointID
20232023
clusterID:(NSNumber * _Nullable)clusterID

src/darwin/Framework/CHIP/MTRDeviceController.mm

+21-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#import "MTRConversion.h"
3333
#import "MTRDeviceControllerDelegateBridge.h"
3434
#import "MTRDeviceControllerFactory_Internal.h"
35+
#import "MTRDeviceControllerLocalTestStorage.h"
3536
#import "MTRDeviceControllerStartupParams.h"
3637
#import "MTRDeviceControllerStartupParams_Internal.h"
3738
#import "MTRDevice_Internal.h"
@@ -173,12 +174,31 @@ - (instancetype)initWithFactory:(MTRDeviceControllerFactory *)factory
173174
return nil;
174175
}
175176

177+
id<MTRDeviceControllerStorageDelegate> storageDelegateToUse = storageDelegate;
178+
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
179+
if (MTRDeviceControllerLocalTestStorage.localTestStorageEnabled) {
180+
storageDelegateToUse = [[MTRDeviceControllerLocalTestStorage alloc] initWithPassThroughStorage:storageDelegate];
181+
}
182+
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
176183
_controllerDataStore = [[MTRDeviceControllerDataStore alloc] initWithController:self
177-
storageDelegate:storageDelegate
184+
storageDelegate:storageDelegateToUse
178185
storageDelegateQueue:storageDelegateQueue];
179186
if (_controllerDataStore == nil) {
180187
return nil;
181188
}
189+
} else {
190+
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
191+
if (MTRDeviceControllerLocalTestStorage.localTestStorageEnabled) {
192+
dispatch_queue_t localTestStorageQueue = dispatch_queue_create("org.csa-iot.matter.framework.devicecontroller.localteststorage", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
193+
MTRDeviceControllerLocalTestStorage * localTestStorage = [[MTRDeviceControllerLocalTestStorage alloc] initWithPassThroughStorage:nil];
194+
_controllerDataStore = [[MTRDeviceControllerDataStore alloc] initWithController:self
195+
storageDelegate:localTestStorage
196+
storageDelegateQueue:localTestStorageQueue];
197+
if (_controllerDataStore == nil) {
198+
return nil;
199+
}
200+
}
201+
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
182202
}
183203

184204
// Ensure the otaProviderDelegate, if any, is valid.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
/**
3+
* Copyright (c) 2023 Project CHIP Authors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#import <Foundation/Foundation.h>
19+
#import <Matter/Matter.h>
20+
21+
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
22+
23+
NS_ASSUME_NONNULL_BEGIN
24+
25+
MTR_EXTERN @interface MTRDeviceControllerLocalTestStorage : NSObject<MTRDeviceControllerStorageDelegate>
26+
27+
// Setting this variable only affects subsequent MTRDeviceController initializations
28+
@property (class, nonatomic, assign) BOOL localTestStorageEnabled;
29+
30+
// This storage persists items to NSUserDefaults for MTRStorageSharingTypeNotShared data. Items with other sharing types will be droppped, or stored/fetched with the "passthrough storage" if one is specified.
31+
- (instancetype)initWithPassThroughStorage:(id<MTRDeviceControllerStorageDelegate> _Nullable)passThroughStorage;
32+
33+
@end
34+
35+
NS_ASSUME_NONNULL_END
36+
37+
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
/**
3+
* Copyright (c) 2023 Project CHIP Authors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#import "MTRDeviceControllerLocalTestStorage.h"
19+
20+
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
21+
22+
static NSString * const kLocalTestUserDefaultDomain = @"org.csa-iot.matter.darwintest";
23+
static NSString * const kLocalTestUserDefaultEnabledKey = @"enableTestStorage";
24+
25+
@implementation MTRDeviceControllerLocalTestStorage {
26+
id<MTRDeviceControllerStorageDelegate> _passThroughStorage;
27+
}
28+
29+
+ (BOOL)localTestStorageEnabled
30+
{
31+
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
32+
return [defaults boolForKey:kLocalTestUserDefaultEnabledKey];
33+
}
34+
35+
+ (void)setLocalTestStorageEnabled:(BOOL)localTestStorageEnabled
36+
{
37+
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
38+
[defaults setBool:localTestStorageEnabled forKey:kLocalTestUserDefaultEnabledKey];
39+
}
40+
41+
- (instancetype)initWithPassThroughStorage:(id<MTRDeviceControllerStorageDelegate>)passThroughStorage
42+
{
43+
if (self = [super init]) {
44+
_passThroughStorage = passThroughStorage;
45+
}
46+
return self;
47+
}
48+
49+
- (nullable id<NSSecureCoding>)controller:(MTRDeviceController *)controller
50+
valueForKey:(NSString *)key
51+
securityLevel:(MTRStorageSecurityLevel)securityLevel
52+
sharingType:(MTRStorageSharingType)sharingType
53+
{
54+
if (sharingType == MTRStorageSharingTypeNotShared) {
55+
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
56+
NSData * storedData = [defaults dataForKey:key];
57+
NSError * error;
58+
id value = [NSKeyedUnarchiver unarchivedObjectOfClasses:MTRDeviceControllerStorageClasses() fromData:storedData error:&error];
59+
return value;
60+
} else {
61+
return [_passThroughStorage controller:controller valueForKey:key securityLevel:securityLevel sharingType:sharingType];
62+
}
63+
}
64+
65+
- (BOOL)controller:(MTRDeviceController *)controller
66+
storeValue:(id<NSSecureCoding>)value
67+
forKey:(NSString *)key
68+
securityLevel:(MTRStorageSecurityLevel)securityLevel
69+
sharingType:(MTRStorageSharingType)sharingType
70+
{
71+
if (sharingType == MTRStorageSharingTypeNotShared) {
72+
NSError * error;
73+
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:value requiringSecureCoding:YES error:&error];
74+
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
75+
[defaults setObject:data forKey:key];
76+
return YES;
77+
} else {
78+
return [_passThroughStorage controller:controller storeValue:value forKey:key securityLevel:securityLevel sharingType:sharingType];
79+
}
80+
}
81+
82+
- (BOOL)controller:(MTRDeviceController *)controller
83+
removeValueForKey:(NSString *)key
84+
securityLevel:(MTRStorageSecurityLevel)securityLevel
85+
sharingType:(MTRStorageSharingType)sharingType
86+
{
87+
if (sharingType == MTRStorageSharingTypeNotShared) {
88+
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
89+
[defaults removeObjectForKey:key];
90+
return YES;
91+
} else {
92+
return [_passThroughStorage controller:controller removeValueForKey:key securityLevel:securityLevel sharingType:sharingType];
93+
}
94+
}
95+
@end
96+
97+
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED

src/darwin/Framework/CHIPTests/MTRDeviceTests.m

+86-15
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
#import <Matter/Matter.h>
2626

2727
#import "MTRCommandPayloadExtensions_Internal.h"
28+
#import "MTRDeviceControllerLocalTestStorage.h"
2829
#import "MTRDeviceTestDelegate.h"
2930
#import "MTRErrorTestUtils.h"
31+
#import "MTRTestDeclarations.h"
3032
#import "MTRTestKeys.h"
3133
#import "MTRTestResetCommissioneeHelper.h"
3234
#import "MTRTestStorage.h"
@@ -74,19 +76,6 @@ static void WaitForCommissionee(XCTestExpectation * expectation)
7476
return mConnectedDevice;
7577
}
7678

77-
#ifdef DEBUG
78-
@interface MTRBaseDevice (Test)
79-
- (void)failSubscribers:(dispatch_queue_t)queue completion:(void (^)(void))completion;
80-
81-
// Test function for whitebox testing
82-
+ (id)CHIPEncodeAndDecodeNSObject:(id)object;
83-
@end
84-
85-
@interface MTRDevice (Test)
86-
- (void)unitTestInjectEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport;
87-
@end
88-
#endif
89-
9079
@interface MTRDeviceTestDeviceControllerDelegate : NSObject <MTRDeviceControllerDelegate>
9180
@property (nonatomic, strong) XCTestExpectation * expectation;
9281
@end
@@ -129,10 +118,19 @@ @interface MTRDeviceTests : XCTestCase
129118

130119
@implementation MTRDeviceTests
131120

121+
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
122+
static BOOL slocalTestStorageEnabledBeforeUnitTest;
123+
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
124+
132125
+ (void)setUp
133126
{
134127
XCTestExpectation * pairingExpectation = [[XCTestExpectation alloc] initWithDescription:@"Pairing Complete"];
135128

129+
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
130+
slocalTestStorageEnabledBeforeUnitTest = MTRDeviceControllerLocalTestStorage.localTestStorageEnabled;
131+
MTRDeviceControllerLocalTestStorage.localTestStorageEnabled = YES;
132+
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
133+
136134
__auto_type * factory = [MTRDeviceControllerFactory sharedInstance];
137135
XCTAssertNotNil(factory);
138136

@@ -182,6 +180,14 @@ + (void)tearDown
182180
{
183181
ResetCommissionee(GetConnectedDevice(), dispatch_get_main_queue(), nil, kTimeoutInSeconds);
184182

183+
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
184+
// Restore testing setting to previous state, and remove all persisted attributes
185+
MTRDeviceControllerLocalTestStorage.localTestStorageEnabled = slocalTestStorageEnabledBeforeUnitTest;
186+
[sController.controllerDataStore clearAllStoredAttributes];
187+
NSArray * storedAttributesAfterClear = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
188+
XCTAssertEqual(storedAttributesAfterClear.count, 0);
189+
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
190+
185191
MTRDeviceController * controller = sController;
186192
XCTAssertNotNil(controller);
187193
[controller shutdown];
@@ -1236,7 +1242,7 @@ - (void)test015_FailedSubscribeWithQueueAcrossShutdown
12361242
__auto_type * params = [[MTRSubscribeParams alloc] init];
12371243
params.resubscribeAutomatically = NO;
12381244
params.replaceExistingSubscriptions = NO; // Not strictly needed, but checking that doing this does not
1239-
// affect this subscription erroring out correctly.
1245+
// affect this subscription erroring out correctly.
12401246
[device subscribeWithQueue:queue
12411247
minInterval:1
12421248
maxInterval:2
@@ -1344,6 +1350,11 @@ - (void)test016_FailedSubscribeWithCacheReadDuringFailure
13441350

13451351
- (void)test017_TestMTRDeviceBasics
13461352
{
1353+
// Ensure the test starts with clean slate, even with MTRDeviceControllerLocalTestStorage enabled
1354+
[sController.controllerDataStore clearAllStoredAttributes];
1355+
NSArray * storedAttributesAfterClear = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
1356+
XCTAssertEqual(storedAttributesAfterClear.count, 0);
1357+
13471358
__auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
13481359
dispatch_queue_t queue = dispatch_get_main_queue();
13491360

@@ -1526,6 +1537,7 @@ - (void)test017_TestMTRDeviceBasics
15261537

15271538
// Resubscription test setup
15281539
XCTestExpectation * subscriptionDroppedExpectation = [self expectationWithDescription:@"Subscription has dropped"];
1540+
15291541
delegate.onNotReachable = ^() {
15301542
[subscriptionDroppedExpectation fulfill];
15311543
};
@@ -1600,7 +1612,7 @@ - (void)test018_SubscriptionErrorWhenNotResubscribing
16001612
MTRSubscribeParams * params = [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(10)];
16011613
params.resubscribeAutomatically = NO;
16021614
params.replaceExistingSubscriptions = NO; // Not strictly needed, but checking that doing this does not
1603-
// affect this subscription erroring out correctly.
1615+
// affect this subscription erroring out correctly.
16041616
__block BOOL subscriptionEstablished = NO;
16051617
[device subscribeToAttributesWithEndpointID:@1
16061618
clusterID:@6
@@ -2826,6 +2838,65 @@ - (void)test030_DeviceAndClusterProperties
28262838
XCTAssertEqualObjects(cluster.endpointID, @(0));
28272839
}
28282840

2841+
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
2842+
- (void)test031_MTRDeviceAttributeCacheLocalTestStorage
2843+
{
2844+
dispatch_queue_t queue = dispatch_get_main_queue();
2845+
2846+
// First start with clean slate and
2847+
__auto_type * device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController];
2848+
[sController removeDevice:device];
2849+
[sController.controllerDataStore clearAllStoredAttributes];
2850+
NSArray * storedAttributesAfterClear = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
2851+
XCTAssertEqual(storedAttributesAfterClear.count, 0);
2852+
2853+
// Now recreate device and get subscription primed
2854+
device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController];
2855+
XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received"];
2856+
__auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
2857+
__weak __auto_type weakDelegate = delegate;
2858+
delegate.onReportEnd = ^{
2859+
[gotReportsExpectation fulfill];
2860+
__strong __auto_type strongDelegate = weakDelegate;
2861+
strongDelegate.onReportEnd = nil;
2862+
};
2863+
[device setDelegate:delegate queue:queue];
2864+
2865+
[self waitForExpectations:@[ gotReportsExpectation ] timeout:60];
2866+
2867+
NSUInteger attributesReportedWithFirstSubscription = [device unitTestAttributesReportedSinceLastCheck];
2868+
2869+
NSArray * dataStoreValuesAfterFirstSubscription = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
2870+
XCTAssertTrue(dataStoreValuesAfterFirstSubscription.count > 0);
2871+
2872+
// Now remove device, resubscribe, and see that it succeeds
2873+
[sController removeDevice:device];
2874+
device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController];
2875+
2876+
XCTestExpectation * resubGotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received for resubscription"];
2877+
delegate.onReportEnd = ^{
2878+
[resubGotReportsExpectation fulfill];
2879+
__strong __auto_type strongDelegate = weakDelegate;
2880+
strongDelegate.onReportEnd = nil;
2881+
};
2882+
[device setDelegate:delegate queue:queue];
2883+
2884+
[self waitForExpectations:@[ resubGotReportsExpectation ] timeout:60];
2885+
2886+
NSUInteger attributesReportedWithSecondSubscription = [device unitTestAttributesReportedSinceLastCheck];
2887+
2888+
XCTAssertTrue(attributesReportedWithSecondSubscription < attributesReportedWithFirstSubscription);
2889+
2890+
// 1) MTRDevice actually gets some attributes reported more than once
2891+
// 2) Some attributes do change on resubscribe
2892+
// * With all-clusts-app as of 2024-02-10, out of 1287 persisted attributes, still 450 attributes were reported with filter
2893+
// And so conservatively, assert that data version filters save at least 300 entries.
2894+
NSArray * dataStoreValuesAfterSecondSubscription = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
2895+
NSUInteger storedAttributeCountDifferenceFromMTRDeviceReport = dataStoreValuesAfterSecondSubscription.count - attributesReportedWithSecondSubscription;
2896+
XCTAssertTrue(storedAttributeCountDifferenceFromMTRDeviceReport > 300);
2897+
}
2898+
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
2899+
28292900
@end
28302901

28312902
@interface MTRDeviceEncoderTests : XCTestCase

src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m

+1-22
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#import "MTRDeviceTestDelegate.h"
2323
#import "MTRErrorTestUtils.h"
2424
#import "MTRFabricInfoChecker.h"
25+
#import "MTRTestDeclarations.h"
2526
#import "MTRTestKeys.h"
2627
#import "MTRTestPerControllerStorage.h"
2728
#import "MTRTestResetCommissioneeHelper.h"
@@ -33,28 +34,6 @@
3334
static NSString * kOnboardingPayload = @"MT:-24J0AFN00KA0648G00";
3435
static const uint16_t kTestVendorId = 0xFFF1u;
3536

36-
#ifdef DEBUG
37-
// MTRDeviceControllerDataStore.h includes C++ header, and so we need to declare the methods separately
38-
@protocol MTRDeviceControllerDataStoreAttributeStoreMethods
39-
- (nullable NSArray<NSDictionary *> *)getStoredAttributesForNodeID:(NSNumber *)nodeID;
40-
- (void)storeAttributeValues:(NSArray<NSDictionary *> *)dataValues forNodeID:(NSNumber *)nodeID;
41-
- (void)clearStoredAttributesForNodeID:(NSNumber *)nodeID;
42-
- (void)clearAllStoredAttributes;
43-
@end
44-
45-
// Declare internal methods for testing
46-
@interface MTRDeviceController (Test)
47-
+ (void)forceLocalhostAdvertisingOnly;
48-
- (void)removeDevice:(MTRDevice *)device;
49-
@property (nonatomic, readonly, nullable) id<MTRDeviceControllerDataStoreAttributeStoreMethods> controllerDataStore;
50-
@end
51-
52-
@interface MTRDevice (Test)
53-
- (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary *)theOther;
54-
- (NSUInteger)unitTestAttributesReportedSinceLastCheck;
55-
@end
56-
#endif // DEBUG
57-
5837
@interface MTRPerControllerStorageTestsControllerDelegate : NSObject <MTRDeviceControllerDelegate>
5938
@property (nonatomic, strong) XCTestExpectation * expectation;
6039
@property (nonatomic, strong) NSNumber * deviceID;

0 commit comments

Comments
 (0)