Skip to content

Commit 02fc6cc

Browse files
Fix historical event detection in MTRDevice.
A few fixes here: * Fix MTRDeviceTests to clean up MTRDevice between tests, so each test starts with a known clean slate. * Fix the "are we getting a priming report?" detection in MTRDevice. Relying on reachability state is not correct, because unsolicited reports can mark us reachable even while we are in the middle of a priming report.
1 parent b653e8e commit 02fc6cc

File tree

2 files changed

+175
-43
lines changed

2 files changed

+175
-43
lines changed

src/darwin/Framework/CHIP/MTRDevice.mm

+37-8
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,34 @@ typedef NS_ENUM(NSUInteger, MTRInternalDeviceState) {
140140
// InitialSubscriptionEstablished means we have at some point finished setting up a
141141
// subscription. That subscription may have dropped since then, but if so it's the ReadClient's
142142
// responsibility to re-establish it.
143-
MTRInternalDeviceStateInitalSubscriptionEstablished = 2,
143+
MTRInternalDeviceStateInitialSubscriptionEstablished = 2,
144+
// Resubscribing means we had established a subscription, but then
145+
// detected a subscription drop due to not receiving a report on time. This
146+
// covers all the actions that happen when re-subscribing (discovery, CASE,
147+
// getting priming reports, etc).
148+
MTRInternalDeviceStateResubscribing = 3,
149+
// LaterSubscriptionEstablished meant that we had a subscription drop and
150+
// then re-created a subscription.
151+
MTRInternalDeviceStateLaterSubscriptionEstablished = 4,
144152
};
145153

146154
// Utility methods for working with MTRInternalDeviceState, located near the
147155
// enum so it's easier to notice that they need to stay in sync.
148156
namespace {
149157
bool HadSubscriptionEstablishedOnce(MTRInternalDeviceState state)
150158
{
151-
return state >= MTRInternalDeviceStateInitalSubscriptionEstablished;
159+
return state >= MTRInternalDeviceStateInitialSubscriptionEstablished;
152160
}
153161

154162
bool NeedToStartSubscriptionSetup(MTRInternalDeviceState state)
155163
{
156164
return state <= MTRInternalDeviceStateUnsubscribed;
157165
}
166+
167+
bool HaveSubscriptionEstablishedRightNow(MTRInternalDeviceState state)
168+
{
169+
return state == MTRInternalDeviceStateInitialSubscriptionEstablished || state == MTRInternalDeviceStateLaterSubscriptionEstablished;
170+
}
158171
} // anonymous namespace
159172

160173
typedef NS_ENUM(NSUInteger, MTRDeviceExpectedValueFieldIndex) {
@@ -878,6 +891,16 @@ - (void)_changeState:(MTRDeviceState)state
878891
}
879892
}
880893

894+
- (void)_changeInternalState:(MTRInternalDeviceState)state
895+
{
896+
os_unfair_lock_assert_owner(&self->_lock);
897+
MTRInternalDeviceState lastState = _internalDeviceState;
898+
_internalDeviceState = state;
899+
if (lastState != state) {
900+
MTR_LOG_DEFAULT("%@ internal state change %lu => %lu", self, static_cast<unsigned long>(lastState), static_cast<unsigned long>(state));
901+
}
902+
}
903+
881904
// First Time Sync happens 2 minutes after reachability (this can be changed in the future)
882905
#define MTR_DEVICE_TIME_UPDATE_INITIAL_WAIT_TIME_SEC (60 * 2)
883906
- (void)_handleSubscriptionEstablished
@@ -886,7 +909,11 @@ - (void)_handleSubscriptionEstablished
886909

887910
// reset subscription attempt wait time when subscription succeeds
888911
_lastSubscriptionAttemptWait = 0;
889-
_internalDeviceState = MTRInternalDeviceStateInitalSubscriptionEstablished;
912+
if (HadSubscriptionEstablishedOnce(_internalDeviceState)) {
913+
[self _changeInternalState:MTRInternalDeviceStateLaterSubscriptionEstablished];
914+
} else {
915+
[self _changeInternalState:MTRInternalDeviceStateInitialSubscriptionEstablished];
916+
}
890917

891918
// As subscription is established, check if the delegate needs to be informed
892919
if (!_delegateDeviceCachePrimedCalled) {
@@ -913,7 +940,7 @@ - (void)_handleSubscriptionError:(NSError *)error
913940
{
914941
std::lock_guard lock(_lock);
915942

916-
_internalDeviceState = MTRInternalDeviceStateUnsubscribed;
943+
[self _changeInternalState:MTRInternalDeviceStateUnsubscribed];
917944
_unreportedEvents = nil;
918945

919946
[self _changeState:MTRDeviceStateUnreachable];
@@ -924,6 +951,7 @@ - (void)_handleResubscriptionNeeded
924951
std::lock_guard lock(_lock);
925952

926953
[self _changeState:MTRDeviceStateUnknown];
954+
[self _changeInternalState:MTRInternalDeviceStateResubscribing];
927955

928956
// If we are here, then the ReadClient either just detected a subscription
929957
// drop or just tried again and failed. Either way, count it as "tried and
@@ -1044,11 +1072,12 @@ - (void)_handleReportBegin
10441072

10451073
_receivingReport = YES;
10461074
if (_state != MTRDeviceStateReachable) {
1047-
_receivingPrimingReport = YES;
10481075
[self _changeState:MTRDeviceStateReachable];
1049-
} else {
1050-
_receivingPrimingReport = NO;
10511076
}
1077+
1078+
// If we currently don't have an established subscription, this must be a
1079+
// priming report.
1080+
_receivingPrimingReport = !HaveSubscriptionEstablishedRightNow(_internalDeviceState);
10521081
}
10531082

10541083
- (NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> *)_clusterDataToPersistSnapshot
@@ -1461,7 +1490,7 @@ - (void)_setupSubscription
14611490
return;
14621491
}
14631492

1464-
_internalDeviceState = MTRInternalDeviceStateSubscribing;
1493+
[self _changeInternalState:MTRInternalDeviceStateSubscribing];
14651494

14661495
// Set up a timer to mark as not reachable if it takes too long to set up a subscription
14671496
MTRWeakReference<MTRDevice *> * weakSelf = [MTRWeakReference weakReferenceWithObject:self];

src/darwin/Framework/CHIPTests/MTRDeviceTests.m

+138-35
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,56 @@ - (void)setUp
218218
{
219219
[super setUp];
220220
[self setContinueAfterFailure:NO];
221+
222+
// Ensure the test starts with clean slate in terms of stored data.
223+
if (sController != nil) {
224+
[sController.controllerDataStore clearAllStoredClusterData];
225+
NSDictionary * storedClusterDataAfterClear = [sController.controllerDataStore getStoredClusterDataForNodeID:@(kDeviceId)];
226+
XCTAssertEqual(storedClusterDataAfterClear.count, 0);
227+
}
228+
}
229+
230+
- (void)tearDown
231+
{
232+
// Make sure our MTRDevice instances, which are stateful, do not keep that
233+
// state between different tests.
234+
if (sController != nil) {
235+
__auto_type * device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController];
236+
[sController removeDevice:device];
237+
}
238+
239+
// Try to make sure we don't have any outstanding subscriptions from
240+
// previous tests, by sending a subscribe request that will get rid of
241+
// existing subscriptions and then fail out due to requesting a subscribe to
242+
// a nonexistent cluster.
243+
if (mConnectedDevice != nil) {
244+
dispatch_queue_t queue = dispatch_get_main_queue();
245+
246+
MTRSubscribeParams * params = [[MTRSubscribeParams alloc] initWithMinInterval:@(0) maxInterval:@(10)];
247+
params.resubscribeAutomatically = NO;
248+
params.replaceExistingSubscriptions = YES;
249+
250+
XCTestExpectation * errorExpectation = [self expectationWithDescription:@"Canceled all subscriptions"];
251+
252+
// There should be no Basic Information cluster on random endpoints.
253+
[mConnectedDevice subscribeToAttributesWithEndpointID:@10000
254+
clusterID:@(MTRClusterIDTypeBasicInformationID)
255+
attributeID:@(0)
256+
params:params
257+
queue:queue
258+
reportHandler:^(id _Nullable values, NSError * _Nullable error) {
259+
XCTAssertNil(values);
260+
XCTAssertNotNil(error);
261+
[errorExpectation fulfill];
262+
}
263+
subscriptionEstablished:^() {
264+
XCTFail("Did not expect subscription to Basic Information on random endpoint to succeed");
265+
}];
266+
267+
[self waitForExpectations:@[ errorExpectation ] timeout:kTimeoutInSeconds];
268+
}
269+
270+
[super tearDown];
221271
}
222272

223273
- (void)test001_ReadAttribute
@@ -406,6 +456,7 @@ - (void)test005_Subscribe
406456
// Subscribe
407457
XCTestExpectation * expectation = [self expectationWithDescription:@"subscribe OnOff attribute"];
408458
__auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(10)];
459+
params.resubscribeAutomatically = NO;
409460
[device subscribeToAttributesWithEndpointID:@1
410461
clusterID:@6
411462
attributeID:@0
@@ -1278,8 +1329,7 @@ - (void)test015_FailedSubscribeWithQueueAcrossShutdown
12781329
__auto_type clusterStateCacheContainer = [[MTRAttributeCacheContainer alloc] init];
12791330
__auto_type * params = [[MTRSubscribeParams alloc] init];
12801331
params.resubscribeAutomatically = NO;
1281-
params.replaceExistingSubscriptions = NO; // Not strictly needed, but checking that doing this does not
1282-
// affect this subscription erroring out correctly.
1332+
params.replaceExistingSubscriptions = NO; // Not strictly needed, but checking that doing this does not affect this subscription erroring out correctly.
12831333
[device subscribeWithQueue:queue
12841334
minInterval:1
12851335
maxInterval:2
@@ -1387,11 +1437,6 @@ - (void)test016_FailedSubscribeWithCacheReadDuringFailure
13871437

13881438
- (void)test017_TestMTRDeviceBasics
13891439
{
1390-
// Ensure the test starts with clean slate, even with MTRDeviceControllerLocalTestStorage enabled
1391-
[sController.controllerDataStore clearAllStoredClusterData];
1392-
NSDictionary * storedClusterDataAfterClear = [sController.controllerDataStore getStoredClusterDataForNodeID:@(kDeviceId)];
1393-
XCTAssertEqual(storedClusterDataAfterClear.count, 0);
1394-
13951440
__auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
13961441
dispatch_queue_t queue = dispatch_get_main_queue();
13971442

@@ -2149,6 +2194,7 @@ - (void)test023_SubscribeMultipleAttributes
21492194
// Subscribe
21502195
XCTestExpectation * expectation = [self expectationWithDescription:@"subscribe OnOff attribute"];
21512196
__auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(10)];
2197+
params.resubscribeAutomatically = NO;
21522198

21532199
NSNumber * failClusterId = @5678;
21542200
NSNumber * failEndpointId = @1000;
@@ -2406,6 +2452,7 @@ - (void)test025_SubscribeMultipleEvents
24062452
// Subscribe
24072453
XCTestExpectation * expectation = [self expectationWithDescription:@"subscribe multiple events"];
24082454
__auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(10)];
2455+
params.resubscribeAutomatically = NO;
24092456

24102457
NSArray<MTREventRequestPath *> * eventPaths = @[
24112458
// Startup event.
@@ -2634,11 +2681,6 @@ - (void)test028_TimeZoneAndDST
26342681

26352682
- (void)test029_MTRDeviceWriteCoalescing
26362683
{
2637-
// Ensure the test starts with clean slate, even with MTRDeviceControllerLocalTestStorage enabled
2638-
[sController.controllerDataStore clearAllStoredClusterData];
2639-
NSDictionary * storedClusterDataAfterClear = [sController.controllerDataStore getStoredClusterDataForNodeID:@(kDeviceId)];
2640-
XCTAssertEqual(storedClusterDataAfterClear.count, 0);
2641-
26422684
__auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
26432685
dispatch_queue_t queue = dispatch_get_main_queue();
26442686

@@ -2971,15 +3013,8 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage
29713013
{
29723014
dispatch_queue_t queue = dispatch_get_main_queue();
29733015

2974-
// First start with clean slate by removing the MTRDevice and clearing the persisted cache
3016+
// Get the subscription primed
29753017
__auto_type * device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController];
2976-
[sController removeDevice:device];
2977-
[sController.controllerDataStore clearAllStoredClusterData];
2978-
NSDictionary * storedClusterDataAfterClear = [sController.controllerDataStore getStoredClusterDataForNodeID:@(kDeviceId)];
2979-
XCTAssertEqual(storedClusterDataAfterClear.count, 0);
2980-
2981-
// Now recreate device and get subscription primed
2982-
device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController];
29833018
XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received"];
29843019
XCTestExpectation * gotDeviceCachePrimed = [self expectationWithDescription:@"Device cache primed for the first time"];
29853020
__auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
@@ -3038,12 +3073,6 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage
30383073
}
30393074
NSUInteger storedAttributeCountDifferenceFromMTRDeviceReport = dataStoreAttributeCountAfterSecondSubscription - attributesReportedWithSecondSubscription;
30403075
XCTAssertTrue(storedAttributeCountDifferenceFromMTRDeviceReport > 300);
3041-
3042-
// We need to remove the device here since the MTRDevice retains its reachable state. So if the next test needs to start with a clean state,
3043-
// it can't do that since the MTRDevice becomes reachable in the previous test. Since there are no changes detected in reachability,
3044-
// the onReachable callback to the delegate is not called.
3045-
// TODO: #33205 Ensure we have a clean slate w.r.t MTRDevice before running each test.
3046-
[sController removeDevice:device];
30473076
}
30483077

30493078
- (void)test032_MTRPathClassesEncoding
@@ -3175,11 +3204,6 @@ + (void)checkAttributeReportTriggersConfigurationChanged:(MTRAttributeIDType)att
31753204

31763205
- (void)test033_TestMTRDeviceDeviceConfigurationChanged
31773206
{
3178-
// Ensure the test starts with clean slate.
3179-
[sController.controllerDataStore clearAllStoredClusterData];
3180-
NSDictionary * storedClusterDataAfterClear = [sController.controllerDataStore getStoredClusterDataForNodeID:@(kDeviceId)];
3181-
XCTAssertEqual(storedClusterDataAfterClear.count, 0);
3182-
31833207
__auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
31843208
dispatch_queue_t queue = dispatch_get_main_queue();
31853209

@@ -3274,7 +3298,7 @@ - (void)test033_TestMTRDeviceDeviceConfigurationChanged
32743298

32753299
[device setDelegate:delegate queue:queue];
32763300

3277-
// Wait for subscription set up and intitial reports received.
3301+
// Wait for subscription set up and initial reports received.
32783302
[self waitForExpectations:@[
32793303
subscriptionExpectation,
32803304
gotInitialReportsExpectation,
@@ -3500,11 +3524,90 @@ - (void)test033_TestMTRDeviceDeviceConfigurationChanged
35003524

35013525
[device unitTestInjectAttributeReport:attributeReport];
35023526
[self waitForExpectations:@[ gotAttributeReportWithMultipleAttributesExpectation, gotAttributeReportWithMultipleAttributesEndExpectation, deviceConfigurationChangedExpectationForAttributeReportWithMultipleAttributes ] timeout:kTimeoutInSeconds];
3527+
}
35033528

3504-
// We need to remove the device here, because we injected data into its attribute cache
3505-
// that does not match the actual server.
3506-
// TODO: #33205 Ensure we have a clean slate w.r.t MTRDevice before running each test.
3529+
- (void)test034_TestMTRDeviceHistoricalEvents
3530+
{
3531+
dispatch_queue_t queue = dispatch_get_main_queue();
3532+
3533+
NSDictionary * storedClusterDataAfterClear = [sController.controllerDataStore getStoredClusterDataForNodeID:@(kDeviceId)];
3534+
XCTAssertEqual(storedClusterDataAfterClear.count, 0);
3535+
3536+
// Set up a subscription via mConnectedDevice that will send us continuous
3537+
// reports.
3538+
XCTestExpectation * firstSubscriptionExpectation = [self expectationWithDescription:@"First subscription established"];
3539+
3540+
MTRSubscribeParams * params = [[MTRSubscribeParams alloc] initWithMinInterval:@(0) maxInterval:@(0)];
3541+
params.resubscribeAutomatically = NO;
3542+
3543+
[mConnectedDevice subscribeToAttributesWithEndpointID:@(0)
3544+
clusterID:@(MTRClusterIDTypeBasicInformationID)
3545+
attributeID:@(0)
3546+
params:params
3547+
queue:queue
3548+
reportHandler:^(id _Nullable values, NSError * _Nullable error) {
3549+
}
3550+
subscriptionEstablished:^() {
3551+
[firstSubscriptionExpectation fulfill];
3552+
}];
3553+
3554+
[self waitForExpectations:@[ firstSubscriptionExpectation ] timeout:kTimeoutInSeconds];
3555+
3556+
// Now set up our MTRDevice and do a subscribe. Make sure all the events we
3557+
// get are marked "historical".
3558+
__auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
3559+
XCTestExpectation * secondSubscriptionExpectation = [self expectationWithDescription:@"Second subscription established"];
3560+
XCTestExpectation * gotFirstReportsExpectation = [self expectationWithDescription:@"First Attribute and Event reports have been received"];
3561+
3562+
__auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
3563+
delegate.onReachable = ^() {
3564+
[secondSubscriptionExpectation fulfill];
3565+
};
3566+
3567+
__block unsigned eventReportsReceived = 0;
3568+
delegate.onEventDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * eventReport) {
3569+
eventReportsReceived += eventReport.count;
3570+
for (NSDictionary<NSString *, id> * eventDict in eventReport) {
3571+
NSNumber * reportIsHistorical = eventDict[MTREventIsHistoricalKey];
3572+
XCTAssertTrue(reportIsHistorical.boolValue);
3573+
}
3574+
};
3575+
3576+
delegate.onReportEnd = ^() {
3577+
[gotFirstReportsExpectation fulfill];
3578+
};
3579+
3580+
[device setDelegate:delegate queue:queue];
3581+
3582+
[self waitForExpectations:@[ secondSubscriptionExpectation, gotFirstReportsExpectation ] timeout:60];
3583+
3584+
// Must have gotten some events (at least StartUp!)
3585+
XCTAssertTrue(eventReportsReceived > 0);
3586+
3587+
// Remove the device, then try again, now with us having stored cluster
3588+
// data. All the events should still be reported as historical.
35073589
[sController removeDevice:device];
3590+
3591+
eventReportsReceived = 0;
3592+
3593+
device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
3594+
XCTestExpectation * thirdSubscriptionExpectation = [self expectationWithDescription:@"Third subscription established"];
3595+
XCTestExpectation * gotSecondReportsExpectation = [self expectationWithDescription:@"Second Attribute and Event reports have been received"];
3596+
3597+
delegate.onReachable = ^() {
3598+
[thirdSubscriptionExpectation fulfill];
3599+
};
3600+
3601+
delegate.onReportEnd = ^() {
3602+
[gotSecondReportsExpectation fulfill];
3603+
};
3604+
3605+
[device setDelegate:delegate queue:queue];
3606+
3607+
[self waitForExpectations:@[ thirdSubscriptionExpectation, gotSecondReportsExpectation ] timeout:60];
3608+
3609+
// Must have gotten some events (at least StartUp!)
3610+
XCTAssertTrue(eventReportsReceived > 0);
35083611
}
35093612

35103613
@end

0 commit comments

Comments
 (0)