Skip to content

Commit

Permalink
[Darwin] MTRDevice attribute cache persistent storage local test faci…
Browse files Browse the repository at this point in the history
…lity (#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
  • Loading branch information
jtung-apple authored Feb 17, 2024
1 parent c13b324 commit 2f2c4f1
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 46 deletions.
2 changes: 1 addition & 1 deletion src/darwin/Framework/CHIP/MTRBaseDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1976,7 +1976,6 @@ - (void)failSubscribers:(dispatch_queue_t)queue completion:(void (^)(void))compl
MTR_LOG_DEBUG("Causing failure in subscribers on purpose");
CauseReadClientFailure(self.deviceController, self.nodeID, queue, completion);
}
#endif

// The following method is for unit testing purpose only
+ (id)CHIPEncodeAndDecodeNSObject:(id)object
Expand Down Expand Up @@ -2018,6 +2017,7 @@ + (id)CHIPEncodeAndDecodeNSObject:(id)object
}
return decodedData.GetDecodedObject();
}
#endif

- (void)readEventsWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
Expand Down
22 changes: 21 additions & 1 deletion src/darwin/Framework/CHIP/MTRDeviceController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#import "MTRConversion.h"
#import "MTRDeviceControllerDelegateBridge.h"
#import "MTRDeviceControllerFactory_Internal.h"
#import "MTRDeviceControllerLocalTestStorage.h"
#import "MTRDeviceControllerStartupParams.h"
#import "MTRDeviceControllerStartupParams_Internal.h"
#import "MTRDevice_Internal.h"
Expand Down Expand Up @@ -173,12 +174,31 @@ - (instancetype)initWithFactory:(MTRDeviceControllerFactory *)factory
return nil;
}

id<MTRDeviceControllerStorageDelegate> storageDelegateToUse = storageDelegate;
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
if (MTRDeviceControllerLocalTestStorage.localTestStorageEnabled) {
storageDelegateToUse = [[MTRDeviceControllerLocalTestStorage alloc] initWithPassThroughStorage:storageDelegate];
}
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
_controllerDataStore = [[MTRDeviceControllerDataStore alloc] initWithController:self
storageDelegate:storageDelegate
storageDelegate:storageDelegateToUse
storageDelegateQueue:storageDelegateQueue];
if (_controllerDataStore == nil) {
return nil;
}
} else {
#if MTR_PER_CONTROLLER_STORAGE_ENABLED
if (MTRDeviceControllerLocalTestStorage.localTestStorageEnabled) {
dispatch_queue_t localTestStorageQueue = dispatch_queue_create("org.csa-iot.matter.framework.devicecontroller.localteststorage", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
MTRDeviceControllerLocalTestStorage * localTestStorage = [[MTRDeviceControllerLocalTestStorage alloc] initWithPassThroughStorage:nil];
_controllerDataStore = [[MTRDeviceControllerDataStore alloc] initWithController:self
storageDelegate:localTestStorage
storageDelegateQueue:localTestStorageQueue];
if (_controllerDataStore == nil) {
return nil;
}
}
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
}

// Ensure the otaProviderDelegate, if any, is valid.
Expand Down
37 changes: 37 additions & 0 deletions src/darwin/Framework/CHIP/MTRDeviceControllerLocalTestStorage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
/**
* Copyright (c) 2023 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import <Foundation/Foundation.h>
#import <Matter/Matter.h>

#if MTR_PER_CONTROLLER_STORAGE_ENABLED

NS_ASSUME_NONNULL_BEGIN

MTR_EXTERN @interface MTRDeviceControllerLocalTestStorage : NSObject<MTRDeviceControllerStorageDelegate>

// Setting this variable only affects subsequent MTRDeviceController initializations
@property (class, nonatomic, assign) BOOL localTestStorageEnabled;

// 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.
- (instancetype)initWithPassThroughStorage:(id<MTRDeviceControllerStorageDelegate> _Nullable)passThroughStorage;

@end

NS_ASSUME_NONNULL_END

#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
97 changes: 97 additions & 0 deletions src/darwin/Framework/CHIP/MTRDeviceControllerLocalTestStorage.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
/**
* Copyright (c) 2023 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import "MTRDeviceControllerLocalTestStorage.h"

#if MTR_PER_CONTROLLER_STORAGE_ENABLED

static NSString * const kLocalTestUserDefaultDomain = @"org.csa-iot.matter.darwintest";
static NSString * const kLocalTestUserDefaultEnabledKey = @"enableTestStorage";

@implementation MTRDeviceControllerLocalTestStorage {
id<MTRDeviceControllerStorageDelegate> _passThroughStorage;
}

+ (BOOL)localTestStorageEnabled
{
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
return [defaults boolForKey:kLocalTestUserDefaultEnabledKey];
}

+ (void)setLocalTestStorageEnabled:(BOOL)localTestStorageEnabled
{
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
[defaults setBool:localTestStorageEnabled forKey:kLocalTestUserDefaultEnabledKey];
}

- (instancetype)initWithPassThroughStorage:(id<MTRDeviceControllerStorageDelegate>)passThroughStorage
{
if (self = [super init]) {
_passThroughStorage = passThroughStorage;
}
return self;
}

- (nullable id<NSSecureCoding>)controller:(MTRDeviceController *)controller
valueForKey:(NSString *)key
securityLevel:(MTRStorageSecurityLevel)securityLevel
sharingType:(MTRStorageSharingType)sharingType
{
if (sharingType == MTRStorageSharingTypeNotShared) {
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
NSData * storedData = [defaults dataForKey:key];
NSError * error;
id value = [NSKeyedUnarchiver unarchivedObjectOfClasses:MTRDeviceControllerStorageClasses() fromData:storedData error:&error];
return value;
} else {
return [_passThroughStorage controller:controller valueForKey:key securityLevel:securityLevel sharingType:sharingType];
}
}

- (BOOL)controller:(MTRDeviceController *)controller
storeValue:(id<NSSecureCoding>)value
forKey:(NSString *)key
securityLevel:(MTRStorageSecurityLevel)securityLevel
sharingType:(MTRStorageSharingType)sharingType
{
if (sharingType == MTRStorageSharingTypeNotShared) {
NSError * error;
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:value requiringSecureCoding:YES error:&error];
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
[defaults setObject:data forKey:key];
return YES;
} else {
return [_passThroughStorage controller:controller storeValue:value forKey:key securityLevel:securityLevel sharingType:sharingType];
}
}

- (BOOL)controller:(MTRDeviceController *)controller
removeValueForKey:(NSString *)key
securityLevel:(MTRStorageSecurityLevel)securityLevel
sharingType:(MTRStorageSharingType)sharingType
{
if (sharingType == MTRStorageSharingTypeNotShared) {
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:kLocalTestUserDefaultDomain];
[defaults removeObjectForKey:key];
return YES;
} else {
return [_passThroughStorage controller:controller removeValueForKey:key securityLevel:securityLevel sharingType:sharingType];
}
}
@end

#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
101 changes: 86 additions & 15 deletions src/darwin/Framework/CHIPTests/MTRDeviceTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
#import <Matter/Matter.h>

#import "MTRCommandPayloadExtensions_Internal.h"
#import "MTRDeviceControllerLocalTestStorage.h"
#import "MTRDeviceTestDelegate.h"
#import "MTRErrorTestUtils.h"
#import "MTRTestDeclarations.h"
#import "MTRTestKeys.h"
#import "MTRTestResetCommissioneeHelper.h"
#import "MTRTestStorage.h"
Expand Down Expand Up @@ -74,19 +76,6 @@ static void WaitForCommissionee(XCTestExpectation * expectation)
return mConnectedDevice;
}

#ifdef DEBUG
@interface MTRBaseDevice (Test)
- (void)failSubscribers:(dispatch_queue_t)queue completion:(void (^)(void))completion;

// Test function for whitebox testing
+ (id)CHIPEncodeAndDecodeNSObject:(id)object;
@end

@interface MTRDevice (Test)
- (void)unitTestInjectEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport;
@end
#endif

@interface MTRDeviceTestDeviceControllerDelegate : NSObject <MTRDeviceControllerDelegate>
@property (nonatomic, strong) XCTestExpectation * expectation;
@end
Expand Down Expand Up @@ -129,10 +118,19 @@ @interface MTRDeviceTests : XCTestCase

@implementation MTRDeviceTests

#if MTR_PER_CONTROLLER_STORAGE_ENABLED
static BOOL slocalTestStorageEnabledBeforeUnitTest;
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED

+ (void)setUp
{
XCTestExpectation * pairingExpectation = [[XCTestExpectation alloc] initWithDescription:@"Pairing Complete"];

#if MTR_PER_CONTROLLER_STORAGE_ENABLED
slocalTestStorageEnabledBeforeUnitTest = MTRDeviceControllerLocalTestStorage.localTestStorageEnabled;
MTRDeviceControllerLocalTestStorage.localTestStorageEnabled = YES;
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED

__auto_type * factory = [MTRDeviceControllerFactory sharedInstance];
XCTAssertNotNil(factory);

Expand Down Expand Up @@ -182,6 +180,14 @@ + (void)tearDown
{
ResetCommissionee(GetConnectedDevice(), dispatch_get_main_queue(), nil, kTimeoutInSeconds);

#if MTR_PER_CONTROLLER_STORAGE_ENABLED
// Restore testing setting to previous state, and remove all persisted attributes
MTRDeviceControllerLocalTestStorage.localTestStorageEnabled = slocalTestStorageEnabledBeforeUnitTest;
[sController.controllerDataStore clearAllStoredAttributes];
NSArray * storedAttributesAfterClear = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
XCTAssertEqual(storedAttributesAfterClear.count, 0);
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED

MTRDeviceController * controller = sController;
XCTAssertNotNil(controller);
[controller shutdown];
Expand Down Expand Up @@ -1236,7 +1242,7 @@ - (void)test015_FailedSubscribeWithQueueAcrossShutdown
__auto_type * params = [[MTRSubscribeParams alloc] init];
params.resubscribeAutomatically = NO;
params.replaceExistingSubscriptions = NO; // Not strictly needed, but checking that doing this does not
// affect this subscription erroring out correctly.
// affect this subscription erroring out correctly.
[device subscribeWithQueue:queue
minInterval:1
maxInterval:2
Expand Down Expand Up @@ -1344,6 +1350,11 @@ - (void)test016_FailedSubscribeWithCacheReadDuringFailure

- (void)test017_TestMTRDeviceBasics
{
// Ensure the test starts with clean slate, even with MTRDeviceControllerLocalTestStorage enabled
[sController.controllerDataStore clearAllStoredAttributes];
NSArray * storedAttributesAfterClear = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
XCTAssertEqual(storedAttributesAfterClear.count, 0);

__auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
dispatch_queue_t queue = dispatch_get_main_queue();

Expand Down Expand Up @@ -1526,6 +1537,7 @@ - (void)test017_TestMTRDeviceBasics

// Resubscription test setup
XCTestExpectation * subscriptionDroppedExpectation = [self expectationWithDescription:@"Subscription has dropped"];

delegate.onNotReachable = ^() {
[subscriptionDroppedExpectation fulfill];
};
Expand Down Expand Up @@ -1600,7 +1612,7 @@ - (void)test018_SubscriptionErrorWhenNotResubscribing
MTRSubscribeParams * params = [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(10)];
params.resubscribeAutomatically = NO;
params.replaceExistingSubscriptions = NO; // Not strictly needed, but checking that doing this does not
// affect this subscription erroring out correctly.
// affect this subscription erroring out correctly.
__block BOOL subscriptionEstablished = NO;
[device subscribeToAttributesWithEndpointID:@1
clusterID:@6
Expand Down Expand Up @@ -2826,6 +2838,65 @@ - (void)test030_DeviceAndClusterProperties
XCTAssertEqualObjects(cluster.endpointID, @(0));
}

#if MTR_PER_CONTROLLER_STORAGE_ENABLED
- (void)test031_MTRDeviceAttributeCacheLocalTestStorage
{
dispatch_queue_t queue = dispatch_get_main_queue();

// First start with clean slate and
__auto_type * device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController];
[sController removeDevice:device];
[sController.controllerDataStore clearAllStoredAttributes];
NSArray * storedAttributesAfterClear = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
XCTAssertEqual(storedAttributesAfterClear.count, 0);

// Now recreate device and get subscription primed
device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController];
XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received"];
__auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
__weak __auto_type weakDelegate = delegate;
delegate.onReportEnd = ^{
[gotReportsExpectation fulfill];
__strong __auto_type strongDelegate = weakDelegate;
strongDelegate.onReportEnd = nil;
};
[device setDelegate:delegate queue:queue];

[self waitForExpectations:@[ gotReportsExpectation ] timeout:60];

NSUInteger attributesReportedWithFirstSubscription = [device unitTestAttributesReportedSinceLastCheck];

NSArray * dataStoreValuesAfterFirstSubscription = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
XCTAssertTrue(dataStoreValuesAfterFirstSubscription.count > 0);

// Now remove device, resubscribe, and see that it succeeds
[sController removeDevice:device];
device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController];

XCTestExpectation * resubGotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received for resubscription"];
delegate.onReportEnd = ^{
[resubGotReportsExpectation fulfill];
__strong __auto_type strongDelegate = weakDelegate;
strongDelegate.onReportEnd = nil;
};
[device setDelegate:delegate queue:queue];

[self waitForExpectations:@[ resubGotReportsExpectation ] timeout:60];

NSUInteger attributesReportedWithSecondSubscription = [device unitTestAttributesReportedSinceLastCheck];

XCTAssertTrue(attributesReportedWithSecondSubscription < attributesReportedWithFirstSubscription);

// 1) MTRDevice actually gets some attributes reported more than once
// 2) Some attributes do change on resubscribe
// * With all-clusts-app as of 2024-02-10, out of 1287 persisted attributes, still 450 attributes were reported with filter
// And so conservatively, assert that data version filters save at least 300 entries.
NSArray * dataStoreValuesAfterSecondSubscription = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
NSUInteger storedAttributeCountDifferenceFromMTRDeviceReport = dataStoreValuesAfterSecondSubscription.count - attributesReportedWithSecondSubscription;
XCTAssertTrue(storedAttributeCountDifferenceFromMTRDeviceReport > 300);
}
#endif // MTR_PER_CONTROLLER_STORAGE_ENABLED

@end

@interface MTRDeviceEncoderTests : XCTestCase
Expand Down
23 changes: 1 addition & 22 deletions src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#import "MTRDeviceTestDelegate.h"
#import "MTRErrorTestUtils.h"
#import "MTRFabricInfoChecker.h"
#import "MTRTestDeclarations.h"
#import "MTRTestKeys.h"
#import "MTRTestPerControllerStorage.h"
#import "MTRTestResetCommissioneeHelper.h"
Expand All @@ -33,28 +34,6 @@
static NSString * kOnboardingPayload = @"MT:-24J0AFN00KA0648G00";
static const uint16_t kTestVendorId = 0xFFF1u;

#ifdef DEBUG
// MTRDeviceControllerDataStore.h includes C++ header, and so we need to declare the methods separately
@protocol MTRDeviceControllerDataStoreAttributeStoreMethods
- (nullable NSArray<NSDictionary *> *)getStoredAttributesForNodeID:(NSNumber *)nodeID;
- (void)storeAttributeValues:(NSArray<NSDictionary *> *)dataValues forNodeID:(NSNumber *)nodeID;
- (void)clearStoredAttributesForNodeID:(NSNumber *)nodeID;
- (void)clearAllStoredAttributes;
@end

// Declare internal methods for testing
@interface MTRDeviceController (Test)
+ (void)forceLocalhostAdvertisingOnly;
- (void)removeDevice:(MTRDevice *)device;
@property (nonatomic, readonly, nullable) id<MTRDeviceControllerDataStoreAttributeStoreMethods> controllerDataStore;
@end

@interface MTRDevice (Test)
- (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary *)theOther;
- (NSUInteger)unitTestAttributesReportedSinceLastCheck;
@end
#endif // DEBUG

@interface MTRPerControllerStorageTestsControllerDelegate : NSObject <MTRDeviceControllerDelegate>
@property (nonatomic, strong) XCTestExpectation * expectation;
@property (nonatomic, strong) NSNumber * deviceID;
Expand Down
Loading

0 comments on commit 2f2c4f1

Please sign in to comment.