Fix support for readAttributePaths: for multiple paths in MTRDeviceOverXPC. (#29767)

Because MTRDevice will batch reads, we need to handle multiple paths here.
diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm
index a37a7dd..6a80560 100644
--- a/src/darwin/Framework/CHIP/MTRDevice.mm
+++ b/src/darwin/Framework/CHIP/MTRDevice.mm
@@ -192,6 +192,8 @@
 #ifdef DEBUG
 @protocol MTRDeviceUnitTestDelegate <MTRDeviceDelegate>
 - (void)unitTestReportEndForDevice:(MTRDevice *)device;
+- (BOOL)unitTestShouldSetUpSubscriptionForDevice:(MTRDevice *)device;
+- (BOOL)unitTestShouldSkipExpectedValuesForWrite:(MTRDevice *)device;
 @end
 #endif
 
@@ -235,11 +237,25 @@
 - (void)setDelegate:(id<MTRDeviceDelegate>)delegate queue:(dispatch_queue_t)queue
 {
     MTR_LOG_INFO("%@ setDelegate %@", self, delegate);
+
+    BOOL setUpSubscription = YES;
+
+    // For unit testing only
+#ifdef DEBUG
+    id testDelegate = delegate;
+    if ([testDelegate respondsToSelector:@selector(unitTestShouldSetUpSubscriptionForDevice:)]) {
+        setUpSubscription = [testDelegate unitTestShouldSetUpSubscriptionForDevice:self];
+    }
+#endif
+
     os_unfair_lock_lock(&self->_lock);
 
     _weakDelegate = [MTRWeakReference weakReferenceWithObject:delegate];
     _delegateQueue = queue;
-    [self _setupSubscription];
+
+    if (setUpSubscription) {
+        [self _setupSubscription];
+    }
 
     os_unfair_lock_unlock(&self->_lock);
 }
@@ -974,7 +990,9 @@
                                 MTR_LOG_ERROR("Read attribute work item [%llu] failed (will retry): %@", workItemID, error);
                                 completion(MTRAsyncWorkNeedsRetry);
                             } else {
-                                MTR_LOG_DEFAULT("Read attribute work item [%llu] failed (giving up): %@", workItemID, error);
+                                if (error) {
+                                    MTR_LOG_DEFAULT("Read attribute work item [%llu] failed (giving up): %@", workItemID, error);
+                                }
                                 completion(MTRAsyncWorkComplete);
                             }
                         }];
@@ -998,13 +1016,25 @@
     expectedValueInterval = MTRClampedNumber(expectedValueInterval, @(1), @(UINT32_MAX));
     MTRAttributePath * attributePath = [MTRAttributePath attributePathWithEndpointID:endpointID
                                                                            clusterID:clusterID
+
                                                                          attributeID:attributeID];
-    // Commit change into expected value cache
-    NSDictionary * newExpectedValueDictionary = @{ MTRAttributePathKey : attributePath, MTRDataKey : value };
-    uint64_t expectedValueID;
-    [self setExpectedValues:@[ newExpectedValueDictionary ]
-        expectedValueInterval:expectedValueInterval
-              expectedValueID:&expectedValueID];
+
+    BOOL useValueAsExpectedValue = YES;
+#ifdef DEBUG
+    id delegate = _weakDelegate.strongObject;
+    if ([delegate respondsToSelector:@selector(unitTestShouldSkipExpectedValuesForWrite:)]) {
+        useValueAsExpectedValue = ![delegate unitTestShouldSkipExpectedValuesForWrite:self];
+    }
+#endif
+
+    uint64_t expectedValueID = 0;
+    if (useValueAsExpectedValue) {
+        // Commit change into expected value cache
+        NSDictionary * newExpectedValueDictionary = @{ MTRAttributePathKey : attributePath, MTRDataKey : value };
+        [self setExpectedValues:@[ newExpectedValueDictionary ]
+            expectedValueInterval:expectedValueInterval
+                  expectedValueID:&expectedValueID];
+    }
 
     MTRAsyncWorkItem * workItem = [[MTRAsyncWorkItem alloc] initWithQueue:self.queue];
     uint64_t workItemID = workItem.uniqueID; // capture only the ID, not the work item
@@ -1026,7 +1056,9 @@
                               completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
                                   if (error) {
                                       MTR_LOG_ERROR("Write attribute work item [%llu] failed: %@", workItemID, error);
-                                      [self removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID];
+                                      if (useValueAsExpectedValue) {
+                                          [self removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID];
+                                      }
                                   }
                                   completion(MTRAsyncWorkComplete);
                               }];
diff --git a/src/darwin/Framework/CHIP/MTRDeviceOverXPC.mm b/src/darwin/Framework/CHIP/MTRDeviceOverXPC.mm
index 529962b..c37754e 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceOverXPC.mm
+++ b/src/darwin/Framework/CHIP/MTRDeviceOverXPC.mm
@@ -139,22 +139,49 @@
                      queue:(dispatch_queue_t)queue
                 completion:(MTRDeviceResponseHandler)completion
 {
-    if (attributePaths == nil || attributePaths.count != 1 || eventPaths != nil) {
-        MTR_LOG_ERROR("MTRBaseDevice doesn't support reading more than a single attribute path at a time over XPC");
+    if (attributePaths == nil || eventPaths != nil) {
+        MTR_LOG_ERROR("MTRBaseDevice doesn't support reading event paths over XPC");
         dispatch_async(queue, ^{
             completion(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil]);
         });
         return;
     }
 
-    auto * path = attributePaths[0];
+    // TODO: Have a better XPC setup for the multiple-paths case, instead of
+    // just converting it into a bunch of separate reads.
+    auto expectedResponses = attributePaths.count;
+    __block decltype(expectedResponses) responses = 0;
+    NSMutableArray<NSDictionary<NSString *, id> *> * seenValues = [[NSMutableArray alloc] init];
+    __block BOOL dispatched = NO;
 
-    [self readAttributesWithEndpointID:path.endpoint
-                             clusterID:path.cluster
-                           attributeID:path.attribute
-                                params:params
-                                 queue:queue
-                            completion:completion];
+    for (MTRAttributeRequestPath * path in attributePaths) {
+        __auto_type singleAttributeResponseHandler = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            if (dispatched) {
+                // We hit an error earlier or something.
+                return;
+            }
+
+            if (error != nil) {
+                dispatched = YES;
+                completion(nil, error);
+                return;
+            }
+
+            [seenValues addObjectsFromArray:values];
+            ++responses;
+            if (responses == expectedResponses) {
+                dispatched = YES;
+                completion([seenValues copy], nil);
+            };
+        };
+
+        [self readAttributesWithEndpointID:path.endpoint
+                                 clusterID:path.cluster
+                               attributeID:path.attribute
+                                    params:params
+                                     queue:queue
+                                completion:singleAttributeResponseHandler];
+    }
 }
 
 - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
diff --git a/src/darwin/Framework/CHIPTests/MTRXPCListenerSampleTests.m b/src/darwin/Framework/CHIPTests/MTRXPCListenerSampleTests.m
index 35403d1..bebed9c 100644
--- a/src/darwin/Framework/CHIPTests/MTRXPCListenerSampleTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRXPCListenerSampleTests.m
@@ -448,6 +448,39 @@
 
 @end
 
+typedef void (^MTRDeviceTestDelegateDataHandler)(NSArray<NSDictionary<NSString *, id> *> *);
+
+@interface MTRXPCDeviceTestDelegate : NSObject <MTRDeviceDelegate>
+@property (nonatomic, nullable) MTRDeviceTestDelegateDataHandler onAttributeDataReceived;
+@end
+
+@implementation MTRXPCDeviceTestDelegate
+- (void)device:(MTRDevice *)device stateChanged:(MTRDeviceState)state
+{
+}
+
+- (void)device:(MTRDevice *)device receivedAttributeReport:(NSArray<NSDictionary<NSString *, id> *> *)attributeReport
+{
+    if (self.onAttributeDataReceived != nil) {
+        self.onAttributeDataReceived(attributeReport);
+    }
+}
+
+- (void)device:(MTRDevice *)device receivedEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport
+{
+}
+
+- (BOOL)unitTestShouldSetUpSubscriptionForDevice:(MTRDevice *)device
+{
+    return NO;
+}
+
+- (BOOL)unitTestShouldSkipExpectedValuesForWrite:(MTRDevice *)device
+{
+    return YES;
+}
+@end
+
 @interface MTRXPCListenerSampleTests : XCTestCase
 
 @end
@@ -465,7 +498,7 @@
         // we're running only one of our test methods (using
         // -only-testing:MatterTests/MTROTAProviderTests/testMethodName), since
         // we did not run test999_TearDown.
-        [self shutdownStack];
+        //        [self shutdownStack];
     }
 }
 
@@ -1565,7 +1598,8 @@
                               }];
     [self waitForExpectationsWithTimeout:kTimeoutInSeconds handler:nil];
 
-#if 0 // The above attribute isn't for timed interaction. Hence, no verification till we have a capable attribute.
+#if 0
+    // The above attribute isn't for timed interaction. Hence, no verification till we have a capable attribute.
     // subscribe, which should get the new value at the timeout
     expectation = [self expectationWithDescription:@"Subscribed"];
     __block void (^reportHandler)(id _Nullable values, NSError * _Nullable error);
@@ -1682,28 +1716,50 @@
     __auto_type * device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:mDeviceController];
     dispatch_queue_t queue = dispatch_get_main_queue();
 
+    __auto_type * delegate = [[MTRXPCDeviceTestDelegate alloc] init];
+    [device setDelegate:delegate queue:queue];
+
     __auto_type * endpoint = @(1);
 
     __auto_type * onOffCluster = [[MTRClusterOnOff alloc] initWithDevice:device endpointID:endpoint queue:queue];
 
-    // Since we have no subscription, reads don't really work right.  We need to
-    // poll for values instead.
-    __auto_type pollForValue = ^(NSNumber * attributeID, NSDictionary<NSString *, id> * (^readBlock)(void), NSNumber * value) {
-        XCTestExpectation * expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Polling for on/off=%@", value]];
+    // The previous tests have left us in a not-so-great state where the device
+    // is (1) on and (2) in the middle of a level move.  Reset to a known state
+    // where the device is off, the level is midway, so it's not doing either on
+    // or off due to the level move, and there is no level move going on.
+    XCTestExpectation * initialOffExpectation = [self expectationWithDescription:@"Turning off to reset to base state"];
+    [onOffCluster offWithParams:nil expectedValues:nil expectedValueInterval:nil completion:^(NSError * error) {
+        XCTAssertNil(error);
+        [initialOffExpectation fulfill];
+    }];
+    [self waitForExpectations:@[ initialOffExpectation ] timeout:kTimeoutInSeconds];
+
+    __auto_type * levelCluster = [[MTRClusterLevelControl alloc] initWithDevice:device endpointID:endpoint queue:queue];
+    XCTestExpectation * initialLevelExpectation = [self expectationWithDescription:@"Setting midpoint level"];
+    __auto_type * params = [[MTRLevelControlClusterMoveToLevelParams alloc] init];
+    params.level = @(128);
+    params.transitionTime = @(0);
+    params.optionsMask = @(MTRLevelControlOptionsExecuteIfOff);
+    params.optionsOverride = @(MTRLevelControlOptionsExecuteIfOff);
+    [levelCluster moveToLevelWithParams:params expectedValues:nil expectedValueInterval:nil completion:^(NSError * error) {
+        XCTAssertNil(error);
+        [initialLevelExpectation fulfill];
+    }];
+    [self waitForExpectations:@[ initialLevelExpectation ] timeout:kTimeoutInSeconds];
+
+    // Since we have no subscription, sync reads don't really work right.  After doing
+    // them, if we get a value that does not match expectation we need to wait for our
+    // delegate to be notified about the attribute.
+    __auto_type waitForValue = ^(NSNumber * attributeID, NSDictionary<NSString *, id> * (^readBlock)(void), NSNumber * value) {
+        XCTestExpectation * expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Waiting for attribute %@=%@", attributeID, value]];
         __auto_type * path = [MTRAttributePath attributePathWithEndpointID:endpoint clusterID:@(MTRClusterIDTypeOnOffID) attributeID:attributeID];
 
-        __block dispatch_block_t poller = ^{
-            __auto_type * attrValue = readBlock();
-            if (attrValue == nil) {
-                dispatch_async(queue, poller);
-                return;
+        __block __auto_type checkValue = ^(NSDictionary<NSString *, id> * responseValue) {
+            if (![path isEqual:responseValue[MTRAttributePathKey]]) {
+                // Not our attribute.
+                return NO;
             }
 
-            __auto_type * responseValue = @{
-                MTRAttributePathKey : path,
-                MTRDataKey : attrValue,
-            };
-
             NSError * error;
             __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:responseValue error:&error];
             XCTAssertNil(error);
@@ -1712,24 +1768,51 @@
             XCTAssertNotNil(report.value);
 
             if ([report.value isEqual:value]) {
-                // Break cycle.
-                poller = nil;
+                delegate.onAttributeDataReceived = nil;
                 [expectation fulfill];
-                return;
+                return YES;
             }
 
-            dispatch_async(queue, poller);
+            // Keep waiting.
+            return NO;
         };
 
-        dispatch_async(queue, poller);
-        [self waitForExpectations:@[ expectation ] timeout:kTimeoutInSeconds + 5];
+        delegate.onAttributeDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * responseValues) {
+            for (NSDictionary<NSString *, id> * responseValue in responseValues) {
+                if (checkValue(responseValue)) {
+                    return;
+                }
+            }
+        };
+
+        __auto_type * attrValue = readBlock();
+        if (attrValue != nil) {
+            __auto_type * responseValue = @{
+                MTRAttributePathKey : path,
+                MTRDataKey : attrValue,
+            };
+
+            checkValue(responseValue);
+        }
+
+        [self waitForExpectations:@[ expectation ] timeout:kTimeoutInSeconds];
     };
 
-    pollForValue(
+    // Wait until the OnOff value is read.  But issue reads for multiple
+    // attributes, so that we test what happens if multiple reads are issued in
+    // a row.  The attribute we care about should be last, so it gets batched
+    // with the others, and we do more than 2 reads so that even if the first
+    // one is dispatched immediately the others batch.  Make sure none of the
+    // other reads involve attributes we care about later in this test.
+    waitForValue(
         @(MTRAttributeIDTypeClusterOnOffAttributeOnOffID), ^{
+            [onOffCluster readAttributeOffWaitTimeWithParams:nil];
+            [onOffCluster readAttributeGlobalSceneControlWithParams:nil];
+            [onOffCluster readAttributeStartUpOnOffWithParams:nil];
             return [onOffCluster readAttributeOnOffWithParams:nil];
         }, @(NO));
-    pollForValue(
+
+    waitForValue(
         @(MTRAttributeIDTypeClusterOnOffAttributeOnTimeID), ^{
             return [onOffCluster readAttributeOnTimeWithParams:nil];
         }, @(0));
@@ -1741,14 +1824,8 @@
     }
                           expectedValueInterval:@(0)];
 
-    // Wait until the expected value is removed.
-    pollForValue(
-        @(MTRAttributeIDTypeClusterOnOffAttributeOnTimeID), ^{
-            return [onOffCluster readAttributeOnTimeWithParams:nil];
-        }, @(0));
-
-    // Now wait until the new value is read.
-    pollForValue(
+    // Wait until the new value is read.
+    waitForValue(
         @(MTRAttributeIDTypeClusterOnOffAttributeOnTimeID), ^{
             return [onOffCluster readAttributeOnTimeWithParams:nil];
         }, @(100));
@@ -1759,17 +1836,11 @@
     }
                           expectedValueInterval:@(0)];
 
-    // Wait until the expected value is removed.
-    pollForValue(
-        @(MTRAttributeIDTypeClusterOnOffAttributeOnTimeID), ^{
-            return [onOffCluster readAttributeOnTimeWithParams:nil];
-        }, @(100));
-
     // Now wait until the new value is read.
-    pollForValue(
+    waitForValue(
         @(MTRAttributeIDTypeClusterOnOffAttributeOnTimeID), ^{
             return [onOffCluster readAttributeOnTimeWithParams:nil];
-        }, @(00));
+        }, @(0));
 
     // Test that invokes work.
     XCTestExpectation * onExpectation = [self expectationWithDescription:@"Turning on"];
@@ -1779,7 +1850,7 @@
     }];
     [self waitForExpectations:@[ onExpectation ] timeout:kTimeoutInSeconds];
 
-    pollForValue(
+    waitForValue(
         @(MTRAttributeIDTypeClusterOnOffAttributeOnOffID), ^{
             return [onOffCluster readAttributeOnOffWithParams:nil];
         }, @(YES));
@@ -1791,7 +1862,7 @@
     }];
     [self waitForExpectations:@[ offExpectation ] timeout:kTimeoutInSeconds];
 
-    pollForValue(
+    waitForValue(
         @(MTRAttributeIDTypeClusterOnOffAttributeOnOffID), ^{
             return [onOffCluster readAttributeOnOffWithParams:nil];
         }, @(NO));