diff --git a/.github/workflows/chef.yaml b/.github/workflows/chef.yaml index b403dffe758a52..7e19768e0bb532 100644 --- a/.github/workflows/chef.yaml +++ b/.github/workflows/chef.yaml @@ -56,7 +56,7 @@ jobs: if: github.actor != 'restyled-io[bot]' container: - image: ghcr.io/project-chip/chip-build-esp32:54 + image: ghcr.io/project-chip/chip-build-esp32:65 options: --user root steps: diff --git a/.github/workflows/examples-esp32.yaml b/.github/workflows/examples-esp32.yaml index 92415c0ce96930..2ac5757d6fee0d 100644 --- a/.github/workflows/examples-esp32.yaml +++ b/.github/workflows/examples-esp32.yaml @@ -36,7 +36,7 @@ jobs: if: github.actor != 'restyled-io[bot]' container: - image: ghcr.io/project-chip/chip-build-esp32:54 + image: ghcr.io/project-chip/chip-build-esp32:65 volumes: - "/tmp/bloat_reports:/tmp/bloat_reports" @@ -126,7 +126,7 @@ jobs: if: github.actor != 'restyled-io[bot]' container: - image: ghcr.io/project-chip/chip-build-esp32:54 + image: ghcr.io/project-chip/chip-build-esp32:65 volumes: - "/tmp/bloat_reports:/tmp/bloat_reports" diff --git a/.github/workflows/qemu.yaml b/.github/workflows/qemu.yaml index bff12a5999586e..d5a03d2667dff7 100644 --- a/.github/workflows/qemu.yaml +++ b/.github/workflows/qemu.yaml @@ -40,7 +40,7 @@ jobs: if: github.actor != 'restyled-io[bot]' container: - image: ghcr.io/project-chip/chip-build-esp32-qemu:54 + image: ghcr.io/project-chip/chip-build-esp32-qemu:65 volumes: - "/tmp/log_output:/tmp/test_logs" diff --git a/.github/workflows/release_artifacts.yaml b/.github/workflows/release_artifacts.yaml index be862403037423..efd9ea7c3deba8 100644 --- a/.github/workflows/release_artifacts.yaml +++ b/.github/workflows/release_artifacts.yaml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest container: - image: ghcr.io/project-chip/chip-build-esp32:54 + image: ghcr.io/project-chip/chip-build-esp32:65 steps: - name: Checkout diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b1e8589403d7f5..d559612d2be60d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -576,6 +576,7 @@ jobs: scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_RVCOPSTATE_2_1.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_RVCOPSTATE_2_3.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_RVCOPSTATE_2_4.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_SC_7_1.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestConformanceSupport.py" --script-args "--trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestMatterTestingSupport.py" --script-args "--trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestSpecParsingSupport.py" --script-args "--trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"' @@ -585,6 +586,7 @@ jobs: scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestSpecParsingDeviceType.py' scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestConformanceSupport.py' scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/test_testing/test_IDM_10_4.py' + scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/test_testing/test_TC_SC_7_1.py' - name: Uploading core files uses: actions/upload-artifact@v4 diff --git a/config/nrfconnect/chip-module/Kconfig b/config/nrfconnect/chip-module/Kconfig index d4ccccd983ed3f..dcd26199f9a1a3 100644 --- a/config/nrfconnect/chip-module/Kconfig +++ b/config/nrfconnect/chip-module/Kconfig @@ -34,15 +34,6 @@ config CHIP_NRF_PLATFORM config CHIP_DEVICE_VENDOR_NAME default "Nordic Semiconductor ASA" -config CHIP_APP_LOG_LEVEL - int "Logging level in application" - default LOG_DEFAULT_LEVEL - depends on LOG - help - Sets the logging level in the Matter application. Use this configuration - option only within the application. To set the logging level for the - Matter stack, use the MATTER_LOG_LEVEL configuration option. - config CHIP_NFC_COMMISSIONING bool "Share onboarding payload in NFC tag" default n diff --git a/config/nxp/chip-module/Kconfig b/config/nxp/chip-module/Kconfig index ae95e1ce348f57..152d1161f9f435 100644 --- a/config/nxp/chip-module/Kconfig +++ b/config/nxp/chip-module/Kconfig @@ -30,15 +30,6 @@ config CHIP_NXP_PLATFORM config CHIP_DEVICE_VENDOR_NAME default "NXP Semiconductors" -config CHIP_APP_LOG_LEVEL - int "Logging level in application" - default LOG_DEFAULT_LEVEL - depends on LOG - help - Sets the logging level in the Matter application. Use this configuration - option only within the application. To set the logging level for the - Matter stack, use the MATTER_LOG_LEVEL configuration option. - config CHIP_EXAMPLE_DEVICE_INFO_PROVIDER bool "Include default device information provider build" default y diff --git a/config/telink/chip-module/Kconfig b/config/telink/chip-module/Kconfig index 46dc36b8973d77..d09f4534835ddf 100644 --- a/config/telink/chip-module/Kconfig +++ b/config/telink/chip-module/Kconfig @@ -22,15 +22,6 @@ if CHIP config CHIP_DEVICE_VENDOR_NAME default "Telink Semiconductor" -config CHIP_APP_LOG_LEVEL - int "Logging level in application" - default LOG_DEFAULT_LEVEL - depends on LOG - help - Sets the logging level in the Matter application. Use this configuration - option only within the application. To set the logging level for the - Matter stack, use the MATTER_LOG_LEVEL configuration option. - config CHIP_NFC_COMMISSIONING bool "Share onboarding payload in NFC tag" default n diff --git a/config/zephyr/Kconfig b/config/zephyr/Kconfig index 95cea7cec1a707..fb06e106e29a38 100644 --- a/config/zephyr/Kconfig +++ b/config/zephyr/Kconfig @@ -42,6 +42,15 @@ menuconfig CHIP if CHIP +config CHIP_APP_LOG_LEVEL + int "Logging level in application" + default LOG_DEFAULT_LEVEL + depends on LOG + help + Sets the logging level in the Matter application. Use this configuration + option only within the application. To set the logging level for the + Matter stack, use the MATTER_LOG_LEVEL configuration option. + # Device and firmware identifers config CHIP_DEVICE_VENDOR_ID diff --git a/config/zephyr/chip-module/Kconfig.logging b/config/zephyr/chip-module/Kconfig.logging new file mode 100644 index 00000000000000..85cf1847a49cce --- /dev/null +++ b/config/zephyr/chip-module/Kconfig.logging @@ -0,0 +1,41 @@ +# +# Copyright (c) 2021 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. +# + +config LOG + default y + +if LOG + +choice LOG_MODE + default LOG_MODE_MINIMAL +endchoice + +choice MATTER_LOG_LEVEL_CHOICE + default MATTER_LOG_LEVEL_DBG +endchoice + +config CHIP_APP_LOG_LEVEL + default 4 # debug + +config LOG_DEFAULT_LEVEL + default 1 # error + +# disable synchronous printk to avoid blocking IRQs which +# may affect time sensitive components +config PRINTK_SYNC + default n + +endif # LOG \ No newline at end of file diff --git a/docs/guides/fabric_synchronization_guide.md b/docs/guides/fabric_synchronization_guide.md index 36107744a97930..1b545ce36ead27 100644 --- a/docs/guides/fabric_synchronization_guide.md +++ b/docs/guides/fabric_synchronization_guide.md @@ -19,6 +19,9 @@ Fabric-Bridge-App example app implements the Aggregator device type with Fabric Synchronization condition met and demonstrates the end-to-end Fabric Synchronization feature using dynamic endpoints. +The Fabric-Admin and Fabric-Bridge-App example applications must be executed on +the same physical device and communicate with each other using RPC. + Fabric Synchronization can be triggered from either side. The initiator of the Fabric Synchronization process, who shares their devices, takes on the Commissioner role. The recipient of the Fabric Synchronization request, who @@ -82,11 +85,29 @@ fabricsync enable-auto-sync 1 Pair the Fabric-Source bridge to Fabric-Sync with node ID 1: ``` -fabricsync add-bridge 1 +fabricsync add-bridge 1 ``` ### Pair Light Example to Fabric-Source +Since Fabric-Bridge also functions as a Matter server, running it alongside the +Light Example app on the same machine would cause conflicts. Therefore, you need +to run the Matter Light Example app on a separate physical machine from the one +hosting Fabric-Admin and Fabric-Bridge. You can then commission the Matter Light +Example app using Fabric-Admin on the source side. + +There is a workaround to avoid conflicts when running multiple Matter server +applications on the same machine, you can use different ports and unique +Key-Value Store (KVS) paths for each app. Here's an example of how to launch a +Light App with custom settings: + +Light App with yet another set of different discriminator/passcode, ports and +KVS + +``` +./out/linux-x64-light-clang/chip-lighting-app --discriminator 3843 --passcode 20202023 --secured-device-port 5543 --unsecured-commissioner-port 5553 --KVS /tmp/chip_kvs_lighting_app +``` + Pair the Light Example with node ID 3 using its payload number: ``` diff --git a/scripts/setup/constraints.txt b/scripts/setup/constraints.txt index 49aff90df0715c..bf0611f4f9eb7c 100644 --- a/scripts/setup/constraints.txt +++ b/scripts/setup/constraints.txt @@ -128,7 +128,7 @@ mobly==1.12.1 # via -r requirements.all.txt msgpack==1.0.4 # via cachecontrol -mypy==0.971 +mypy==1.10.1 # via -r requirements.all.txt mypy-extensions==1.0.0 # via mypy diff --git a/scripts/setup/requirements.all.txt b/scripts/setup/requirements.all.txt index 3266eaf8504572..346abfa3936888 100644 --- a/scripts/setup/requirements.all.txt +++ b/scripts/setup/requirements.all.txt @@ -38,7 +38,7 @@ appdirs coloredlogs watchdog build==0.8.0 -mypy==0.971 +mypy==1.10.1 mypy-protobuf==3.5.0 protobuf==4.24.4 types-protobuf==4.24.0.2 @@ -50,4 +50,3 @@ colorama # update tornado for pw_watch tornado - diff --git a/src/app/tests/suites/certification/Test_TC_S_2_2.yaml b/src/app/tests/suites/certification/Test_TC_S_2_2.yaml index ead80fedb78436..81011a13251a70 100644 --- a/src/app/tests/suites/certification/Test_TC_S_2_2.yaml +++ b/src/app/tests/suites/certification/Test_TC_S_2_2.yaml @@ -81,7 +81,20 @@ tests: value: maxScenesMinusOne / 2 - label: - "Step 0a :TH sends KeySetWrite command in the GroupKeyManagement + "Step 0a :TH reads attribute {ServerList} from the Descriptor cluster + of the endpoint that implements the Scenes Management server on the + DUT. DUT responds with a list of server clusters containing the groups + cluster." + cluster: "Descriptor" + command: "readAttribute" + attribute: "ServerList" + response: + constraints: + type: list + contains: [4] + + - label: + "Step 0b :TH sends KeySetWrite command in the GroupKeyManagement cluster to DUT using a key that is pre-installed on the TH. GroupKeySet fields are as follows:" cluster: "Group Key Management" @@ -103,7 +116,7 @@ tests: } - label: - "Step 0b: TH binds GroupIds 0x0001 and 0x0002 with GroupKeySetID + "Step 0c: TH binds GroupIds 0x0001 and 0x0002 with GroupKeySetID 0x01a1 in the GroupKeyMap attribute list on GroupKeyManagement cluster by writing the GroupKeyMap attribute with two entries as follows:" cluster: "Group Key Management" @@ -117,7 +130,7 @@ tests: { FabricIndex: 1, GroupId: G2, GroupKeySetID: 0x01a1 }, ] - - label: "Step 0c: TH sends a RemoveAllGroups command to DUT." + - label: "Step 0d: TH sends a RemoveAllGroups command to DUT." PICS: G.S.C04.Rsp cluster: "Groups" endpoint: endpoint diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 93f96398b51c2f..05bc9f0ee6b0ac 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -908,12 +908,17 @@ - (void)invalidate _reattemptingSubscription = NO; [_deviceController asyncDispatchToMatterQueue:^{ + MTR_LOG("%@ invalidate disconnecting ReadClient and SubscriptionCallback", self); + // Destroy the read client and callback (has to happen on the Matter // queue, to avoid deleting objects that are being referenced), to // tear down the subscription. We will get no more callbacks from // the subscrption after this point. std::lock_guard lock(self->_lock); self->_currentReadClient = nullptr; + if (self->_currentSubscriptionCallback) { + delete self->_currentSubscriptionCallback; + } self->_currentSubscriptionCallback = nullptr; [self _changeInternalState:MTRInternalDeviceStateUnsubscribed]; @@ -940,6 +945,7 @@ - (void)nodeMayBeAdvertisingOperational // whether it might be. - (void)_triggerResubscribeWithReason:(NSString *)reason nodeLikelyReachable:(BOOL)nodeLikelyReachable { + MTR_LOG("%@ _triggerResubscribeWithReason called with reason %@", self, reason); assertChipStackLockedByCurrentThread(); // We might want to trigger a resubscribe on our existing ReadClient. Do @@ -1235,6 +1241,12 @@ - (void)_handleSubscriptionEstablished - (void)_handleSubscriptionError:(NSError *)error { std::lock_guard lock(_lock); + [self _doHandleSubscriptionError:error]; +} + +- (void)_doHandleSubscriptionError:(NSError *)error +{ + os_unfair_lock_assert_owner(&_lock); [self _changeInternalState:MTRInternalDeviceStateUnsubscribed]; _unreportedEvents = nil; @@ -1400,6 +1412,12 @@ - (void)_handleResubscriptionNeededWithDelay:(NSNumber *)resubscriptionDelayMs - (void)_handleSubscriptionReset:(NSNumber * _Nullable)retryDelay { std::lock_guard lock(_lock); + [self _doHandleSubscriptionReset:retryDelay]; +} + +- (void)_doHandleSubscriptionReset:(NSNumber * _Nullable)retryDelay +{ + os_unfair_lock_assert_owner(&_lock); // If we are here, then either we failed to establish initial CASE, or we // failed to send the initial SubscribeRequest message, or our ReadClient @@ -1471,7 +1489,7 @@ - (void)_reattemptSubscriptionNowIfNeededWithReason:(NSString *)reason return; } - MTR_LOG("%@ reattempting subscription", self); + MTR_LOG("%@ reattempting subscription with reason %@", self, reason); self.reattemptingSubscription = NO; [self _setupSubscriptionWithReason:reason]; } @@ -2100,6 +2118,22 @@ - (void)unitTestClearClusterData } #endif +- (void)_reconcilePersistedClustersWithStorage +{ + os_unfair_lock_assert_owner(&self->_lock); + + NSMutableSet * clusterPathsToRemove = [NSMutableSet set]; + for (MTRClusterPath * clusterPath in _persistedClusters) { + MTRDeviceClusterData * data = [_deviceController.controllerDataStore getStoredClusterDataForNodeID:_nodeID endpointID:clusterPath.endpoint clusterID:clusterPath.cluster]; + if (!data) { + [clusterPathsToRemove addObject:clusterPath]; + } + } + + MTR_LOG_ERROR("%@ Storage missing %lu / %lu clusters - reconciling in-memory records", self, static_cast(clusterPathsToRemove.count), static_cast(_persistedClusters.count)); + [_persistedClusters minusSet:clusterPathsToRemove]; +} + - (nullable MTRDeviceClusterData *)_clusterDataForPath:(MTRClusterPath *)clusterPath { os_unfair_lock_assert_owner(&self->_lock); @@ -2132,8 +2166,16 @@ - (nullable MTRDeviceClusterData *)_clusterDataForPath:(MTRClusterPath *)cluster // Page in the stored value for the data. MTRDeviceClusterData * data = [_deviceController.controllerDataStore getStoredClusterDataForNodeID:_nodeID endpointID:clusterPath.endpoint clusterID:clusterPath.cluster]; + MTR_LOG("%@ cluster path %@ cache miss - load from storage success %@", self, clusterPath, YES_NO(data)); if (data != nil) { [_persistedClusterData setObject:data forKey:clusterPath]; + } else { + // If clusterPath is in _persistedClusters and the data store returns nil for it, then the in-memory cache is now not dependable, and subscription should be reset and reestablished to reload cache from device + + // First make sure _persistedClusters is consistent with storage, so repeated calls don't immediately re-trigger this + [self _reconcilePersistedClustersWithStorage]; + + [self _resetSubscriptionWithReasonString:[NSString stringWithFormat:@"Data store has no data for cluster %@", clusterPath]]; } return data; @@ -2303,13 +2345,43 @@ - (void)_stopConnectivityMonitoring } } +- (void)_resetSubscriptionWithReasonString:(NSString *)reasonString +{ + os_unfair_lock_assert_owner(&self->_lock); + MTR_LOG_ERROR("%@ %@ - resetting subscription", self, reasonString); + + [_deviceController asyncDispatchToMatterQueue:^{ + MTR_LOG("%@ subscription reset disconnecting ReadClient and SubscriptionCallback", self); + + std::lock_guard lock(self->_lock); + self->_currentReadClient = nullptr; + if (self->_currentSubscriptionCallback) { + delete self->_currentSubscriptionCallback; + } + self->_currentSubscriptionCallback = nullptr; + + [self _doHandleSubscriptionError:nil]; + // Use nil reset delay so that this keeps existing backoff timing + [self _doHandleSubscriptionReset:nil]; + } + errorHandler:nil]; +} + +#ifdef DEBUG +- (void)unitTestResetSubscription +{ + std::lock_guard lock(self->_lock); + [self _resetSubscriptionWithReasonString:@"Unit test reset subscription"]; +} +#endif + // assume lock is held - (void)_setupSubscriptionWithReason:(NSString *)reason { os_unfair_lock_assert_owner(&self->_lock); if (![self _subscriptionsAllowed]) { - MTR_LOG("%@ _setupSubscription: Subscriptions not allowed. Do not set up subscription", self); + MTR_LOG("%@ _setupSubscription: Subscriptions not allowed. Do not set up subscription (reason: %@)", self, reason); return; } @@ -2328,6 +2400,7 @@ - (void)_setupSubscriptionWithReason:(NSString *)reason // for now just subscribe once if (!NeedToStartSubscriptionSetup(_internalDeviceState)) { + MTR_LOG("%@ setupSubscription: no need to subscribe due to internal state %lu (reason: %@)", self, static_cast(_internalDeviceState), reason); return; } @@ -3758,14 +3831,21 @@ - (void)_storePersistedDeviceData } #ifdef DEBUG -- (MTRDeviceClusterData *)_getClusterDataForPath:(MTRClusterPath *)path +- (MTRDeviceClusterData *)unitTestGetClusterDataForPath:(MTRClusterPath *)path { std::lock_guard lock(_lock); return [[self _clusterDataForPath:path] copy]; } -- (BOOL)_clusterHasBeenPersisted:(MTRClusterPath *)path +- (NSSet *)unitTestGetPersistedClusters +{ + std::lock_guard lock(_lock); + + return [_persistedClusters copy]; +} + +- (BOOL)unitTestClusterHasBeenPersisted:(MTRClusterPath *)path { std::lock_guard lock(_lock); diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.mm b/src/darwin/Framework/CHIP/MTRDeviceController.mm index 0f263d756c5e7b..2e4bb6d4fb0400 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceController.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceController.mm @@ -272,7 +272,7 @@ - (instancetype)initWithFactory:(MTRDeviceControllerFactory *)factory concurrentSubscriptionPoolSize = 1; } - MTR_LOG("Setting up pool size of MTRDeviceController with: %lu", static_cast(concurrentSubscriptionPoolSize)); + MTR_LOG("%@ Setting up pool size of MTRDeviceController with: %lu", self, static_cast(concurrentSubscriptionPoolSize)); _concurrentSubscriptionPool = [[MTRAsyncWorkQueue alloc] initWithContext:self width:concurrentSubscriptionPoolSize]; @@ -283,6 +283,11 @@ - (instancetype)initWithFactory:(MTRDeviceControllerFactory *)factory return self; } +- (NSString *)description +{ + return [NSString stringWithFormat:@"", self, _uniqueIdentifier]; +} + - (BOOL)isRunning { return _cppCommissioner != nullptr; @@ -290,6 +295,7 @@ - (BOOL)isRunning - (void)shutdown { + MTR_LOG("%@ shutdown called", self); if (_cppCommissioner == nullptr) { // Already shut down. return; @@ -393,7 +399,7 @@ - (BOOL)startup:(MTRDeviceControllerStartupParamsInternal *)startupParams { __block BOOL commissionerInitialized = NO; if ([self isRunning]) { - MTR_LOG_ERROR("Unexpected duplicate call to startup"); + MTR_LOG_ERROR("%@ Unexpected duplicate call to startup", self); return NO; } @@ -404,24 +410,24 @@ - (BOOL)startup:(MTRDeviceControllerStartupParamsInternal *)startupParams if (startupParams.vendorID == nil || [startupParams.vendorID unsignedShortValue] == chip::VendorId::Common) { // Shouldn't be using the "standard" vendor ID for actual devices. - MTR_LOG_ERROR("%@ is not a valid vendorID to initialize a device controller with", startupParams.vendorID); + MTR_LOG_ERROR("%@ %@ is not a valid vendorID to initialize a device controller with", self, startupParams.vendorID); return; } if (startupParams.operationalCertificate == nil && startupParams.nodeID == nil) { - MTR_LOG_ERROR("Can't start a controller if we don't know what node id it is"); + MTR_LOG_ERROR("%@ Can't start a controller if we don't know what node id it is", self); return; } if ([startupParams keypairsMatchCertificates] == NO) { - MTR_LOG_ERROR("Provided keypairs do not match certificates"); + MTR_LOG_ERROR("%@ Provided keypairs do not match certificates", self); return; } if (startupParams.operationalCertificate != nil && startupParams.operationalKeypair == nil && (!startupParams.fabricIndex.HasValue() || !startupParams.keystore->HasOpKeypairForFabric(startupParams.fabricIndex.Value()))) { - MTR_LOG_ERROR("Have no operational keypair for our operational certificate"); + MTR_LOG_ERROR("%@ Have no operational keypair for our operational certificate", self); return; } @@ -584,9 +590,12 @@ - (BOOL)startup:(MTRDeviceControllerStartupParamsInternal *)startupParams self->_storedFabricIndex = fabricIdx; commissionerInitialized = YES; + + MTR_LOG("%@ startup succeeded for nodeID 0x%016llX", self, self->_cppCommissioner->GetNodeId()); }); if (commissionerInitialized == NO) { + MTR_LOG_ERROR("%@ startup failed", self); [self cleanupAfterStartup]; return NO; } @@ -597,7 +606,7 @@ - (BOOL)startup:(MTRDeviceControllerStartupParamsInternal *)startupParams // above. if (![self setOperationalCertificateIssuer:startupParams.operationalCertificateIssuer queue:startupParams.operationalCertificateIssuerQueue]) { - MTR_LOG_ERROR("operationalCertificateIssuer and operationalCertificateIssuerQueue must both be nil or both be non-nil"); + MTR_LOG_ERROR("%@ operationalCertificateIssuer and operationalCertificateIssuerQueue must both be nil or both be non-nil", self); [self cleanupAfterStartup]; return NO; } @@ -605,14 +614,14 @@ - (BOOL)startup:(MTRDeviceControllerStartupParamsInternal *)startupParams if (_controllerDataStore) { // If the storage delegate supports the bulk read API, then a dictionary of nodeID => cluster data dictionary would be passed to the handler. Otherwise this would be a no-op, and stored attributes for MTRDevice objects will be loaded lazily in -deviceForNodeID:. [_controllerDataStore fetchAttributeDataForAllDevices:^(NSDictionary *> * _Nonnull clusterDataByNode) { - MTR_LOG("Loaded attribute values for %lu nodes from storage for controller uuid %@", static_cast(clusterDataByNode.count), self->_uniqueIdentifier); + MTR_LOG("%@ Loaded attribute values for %lu nodes from storage for controller uuid %@", self, static_cast(clusterDataByNode.count), self->_uniqueIdentifier); std::lock_guard lock(self->_deviceMapLock); NSMutableArray * deviceList = [NSMutableArray array]; for (NSNumber * nodeID in clusterDataByNode) { NSDictionary * clusterData = clusterDataByNode[nodeID]; MTRDevice * device = [self _setupDeviceForNodeID:nodeID prefetchedClusterData:clusterData]; - MTR_LOG("Loaded %lu cluster data from storage for %@", static_cast(clusterData.count), device); + MTR_LOG("%@ Loaded %lu cluster data from storage for %@", self, static_cast(clusterData.count), device); [deviceList addObject:device]; } @@ -623,7 +632,7 @@ - (BOOL)startup:(MTRDeviceControllerStartupParamsInternal *)startupParams // Note that this is just an optimization to avoid throwing the information away and immediately // re-reading it from storage. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (kSecondsToWaitBeforeAPIClientRetainsMTRDevice * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - MTR_LOG("MTRDeviceController: un-retain devices loaded at startup %lu", static_cast(deviceList.count)); + MTR_LOG("%@ un-retain devices loaded at startup %lu", self, static_cast(deviceList.count)); }); }]; } @@ -638,7 +647,7 @@ - (NSNumber *)controllerNodeID NSNumber * nodeID = [self syncRunOnWorkQueueWithReturnValue:block error:nil]; if (!nodeID) { - MTR_LOG_ERROR("A controller has no node id if it has not been started"); + MTR_LOG_ERROR("%@ A controller has no node id if it has not been started", self); } return nodeID; @@ -707,7 +716,7 @@ - (BOOL)setupCommissioningSessionWithDiscoveredDevice:(MTRCommissionableBrowserR newNodeID:(NSNumber *)newNodeID error:(NSError * __autoreleasing *)error { - MTR_LOG("Setting up commissioning session for already-discovered device %@ and device ID 0x%016llX with setup payload %@", discoveredDevice, newNodeID.unsignedLongLongValue, payload); + MTR_LOG("%@ Setting up commissioning session for already-discovered device %@ and device ID 0x%016llX with setup payload %@", self, discoveredDevice, newNodeID.unsignedLongLongValue, payload); [[MTRMetricsCollector sharedInstance] resetMetrics]; @@ -965,8 +974,7 @@ - (MTRBaseDevice *)deviceBeingCommissionedWithNodeID:(NSNumber *)nodeID error:(N }; MTRBaseDevice * device = [self syncRunOnWorkQueueWithReturnValue:block error:error]; - MTR_LOG("Getting device being commissioned with node ID 0x%016llX: %@ (error: %@)", - nodeID.unsignedLongLongValue, device, (error ? *error : nil)); + MTR_LOG("%@ Getting device being commissioned with node ID 0x%016llX: %@ (error: %@)", self, nodeID.unsignedLongLongValue, device, (error ? *error : nil)); return device; } @@ -996,7 +1004,7 @@ - (MTRDevice *)_setupDeviceForNodeID:(NSNumber *)nodeID prefetchedClusterData:(N } else if (_controllerDataStore) { // Load persisted cluster data if they exist. NSDictionary * clusterData = [_controllerDataStore getStoredClusterDataForNodeID:nodeID]; - MTR_LOG("Loaded %lu cluster data from storage for %@", static_cast(clusterData.count), deviceToReturn); + MTR_LOG("%@ Loaded %lu cluster data from storage for %@", self, static_cast(clusterData.count), deviceToReturn); if (clusterData.count) { [deviceToReturn setPersistedClusterData:clusterData]; } @@ -1035,7 +1043,7 @@ - (void)removeDevice:(MTRDevice *)device [deviceToRemove invalidate]; [_nodeIDToDeviceMap removeObjectForKey:nodeID]; } else { - MTR_LOG_ERROR("Error: Cannot remove device %p with nodeID %llu", device, nodeID.unsignedLongLongValue); + MTR_LOG_ERROR("%@ Error: Cannot remove device %p with nodeID %llu", self, device, nodeID.unsignedLongLongValue); } } @@ -1143,7 +1151,7 @@ - (BOOL)addServerEndpoint:(MTRServerEndpoint *)endpoint } if (![endpoint associateWithController:self]) { - MTR_LOG_ERROR("Failed to associate MTRServerEndpoint with MTRDeviceController"); + MTR_LOG_ERROR("%@ Failed to associate MTRServerEndpoint with MTRDeviceController", self); [_factory removeServerEndpoint:endpoint]; return NO; } @@ -1151,11 +1159,11 @@ - (BOOL)addServerEndpoint:(MTRServerEndpoint *)endpoint [self asyncDispatchToMatterQueue:^() { [self->_serverEndpoints addObject:endpoint]; [endpoint registerMatterEndpoint]; - MTR_LOG("Added server endpoint %u to controller %@", static_cast(endpoint.endpointID.unsignedLongLongValue), + MTR_LOG("%@ Added server endpoint %u to controller %@", self, static_cast(endpoint.endpointID.unsignedLongLongValue), self->_uniqueIdentifier); } errorHandler:^(NSError * error) { - MTR_LOG_ERROR("Unexpected failure dispatching to Matter queue on running controller in addServerEndpoint, adding endpoint %u", + MTR_LOG_ERROR("%@ Unexpected failure dispatching to Matter queue on running controller in addServerEndpoint, adding endpoint %u", self, static_cast(endpoint.endpointID.unsignedLongLongValue)); }]; return YES; @@ -1179,7 +1187,7 @@ - (void)removeServerEndpointInternal:(MTRServerEndpoint *)endpoint queue:(dispat // tearing it down. [self asyncDispatchToMatterQueue:^() { [self removeServerEndpointOnMatterQueue:endpoint]; - MTR_LOG("Removed server endpoint %u from controller %@", static_cast(endpoint.endpointID.unsignedLongLongValue), + MTR_LOG("%@ Removed server endpoint %u from controller %@", self, static_cast(endpoint.endpointID.unsignedLongLongValue), self->_uniqueIdentifier); if (queue != nil && completion != nil) { dispatch_async(queue, completion); @@ -1187,7 +1195,7 @@ - (void)removeServerEndpointInternal:(MTRServerEndpoint *)endpoint queue:(dispat } errorHandler:^(NSError * error) { // Error means we got shut down, so the endpoint is removed now. - MTR_LOG("controller %@ already shut down, so endpoint %u has already been removed", self->_uniqueIdentifier, + MTR_LOG("%@ controller already shut down, so endpoint %u has already been removed", self, static_cast(endpoint.endpointID.unsignedLongLongValue)); if (queue != nil && completion != nil) { dispatch_async(queue, completion); @@ -1212,7 +1220,7 @@ - (BOOL)checkForInitError:(BOOL)condition logMsg:(NSString *)logMsg return NO; } - MTR_LOG_ERROR("Error: %@", logMsg); + MTR_LOG_ERROR("%@ Error: %@", self, logMsg); [self cleanup]; @@ -1233,7 +1241,7 @@ - (BOOL)checkForStartError:(CHIP_ERROR)errorCode logMsg:(NSString *)logMsg return NO; } - MTR_LOG_ERROR("Error(%" CHIP_ERROR_FORMAT "): %@", errorCode.Format(), logMsg); + MTR_LOG_ERROR("Error(%" CHIP_ERROR_FORMAT "): %@ %@", errorCode.Format(), self, logMsg); return YES; } @@ -1244,7 +1252,7 @@ + (BOOL)checkForError:(CHIP_ERROR)errorCode logMsg:(NSString *)logMsg error:(NSE return NO; } - MTR_LOG_ERROR("Error(%" CHIP_ERROR_FORMAT "): %s", errorCode.Format(), [logMsg UTF8String]); + MTR_LOG_ERROR("Error(%" CHIP_ERROR_FORMAT "): %@ %s", errorCode.Format(), self, [logMsg UTF8String]); if (error) { *error = [MTRError errorForCHIPErrorCode:errorCode]; } @@ -1882,7 +1890,7 @@ - (MTRBaseDevice *)getDeviceBeingCommissioned:(uint64_t)deviceId error:(NSError - (BOOL)openPairingWindow:(uint64_t)deviceID duration:(NSUInteger)duration error:(NSError * __autoreleasing *)error { if (duration > UINT16_MAX) { - MTR_LOG_ERROR("Error: Duration %lu is too large. Max value %d", static_cast(duration), UINT16_MAX); + MTR_LOG_ERROR("%@ Error: Duration %lu is too large. Max value %d", self, static_cast(duration), UINT16_MAX); if (error) { *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE]; } @@ -1908,7 +1916,7 @@ - (NSString *)openPairingWindowWithPIN:(uint64_t)deviceID error:(NSError * __autoreleasing *)error { if (duration > UINT16_MAX) { - MTR_LOG_ERROR("Error: Duration %lu is too large. Max value %d", static_cast(duration), UINT16_MAX); + MTR_LOG_ERROR("%@ Error: Duration %lu is too large. Max value %d", self, static_cast(duration), UINT16_MAX); if (error) { *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE]; } @@ -1916,7 +1924,7 @@ - (NSString *)openPairingWindowWithPIN:(uint64_t)deviceID } if (discriminator > 0xfff) { - MTR_LOG_ERROR("Error: Discriminator %lu is too large. Max value %d", static_cast(discriminator), 0xfff); + MTR_LOG_ERROR("%@ Error: Discriminator %lu is too large. Max value %d", self, static_cast(discriminator), 0xfff); if (error) { *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE]; } @@ -1927,7 +1935,7 @@ - (NSString *)openPairingWindowWithPIN:(uint64_t)deviceID MATTER_LOG_METRIC_SCOPE(kMetricOpenPairingWindow, errorCode); if (!chip::CanCastTo(setupPIN) || !chip::SetupPayload::IsValidSetupPIN(static_cast(setupPIN))) { - MTR_LOG_ERROR("Error: Setup pin %lu is not valid", static_cast(setupPIN)); + MTR_LOG_ERROR("%@ Error: Setup pin %lu is not valid", self, static_cast(setupPIN)); errorCode = CHIP_ERROR_INVALID_INTEGER_VALUE; if (error) { *error = [MTRError errorForCHIPErrorCode:errorCode]; @@ -1949,11 +1957,11 @@ - (NSString *)openPairingWindowWithPIN:(uint64_t)deviceID std::string outCode; if (CHIP_NO_ERROR != (errorCode = generator.payloadDecimalStringRepresentation(outCode))) { - MTR_LOG_ERROR("Failed to get decimal setup code"); + MTR_LOG_ERROR("%@ Failed to get decimal setup code", self); return nil; } - MTR_LOG_ERROR("Setup code is %s", outCode.c_str()); + MTR_LOG_ERROR("%@ Setup code is %s", self, outCode.c_str()); return [NSString stringWithCString:outCode.c_str() encoding:[NSString defaultCStringEncoding]]; }; diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm index a328b2a903627f..8d57a5b0a1927d 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm @@ -1011,6 +1011,7 @@ - (void)storeClusterData:(NSDictionary BOOL endpointIndexModified = NO; NSMutableArray * endpointIndexToStore; if (endpointIndex) { + MTR_LOG("No entry found for endpointIndex @ node 0x%016llX - creating", nodeID.unsignedLongLongValue); endpointIndexToStore = [endpointIndex mutableCopy]; } else { endpointIndexToStore = [NSMutableArray array]; @@ -1029,6 +1030,7 @@ - (void)storeClusterData:(NSDictionary BOOL clusterIndexModified = NO; NSMutableArray * clusterIndexToStore; if (clusterIndex) { + MTR_LOG("No entry found for clusterIndex @ node 0x%016llX endpoint %u - creating", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); clusterIndexToStore = [clusterIndex mutableCopy]; } else { clusterIndexToStore = [NSMutableArray array]; @@ -1074,6 +1076,7 @@ - (void)storeClusterData:(NSDictionary NSArray * nodeIndexToStore = nil; if (!nodeIndex) { // Ensure node index exists + MTR_LOG("No entry found for for nodeIndex - creating for node 0x%016llX", nodeID.unsignedLongLongValue); nodeIndexToStore = [NSArray arrayWithObject:nodeID]; } else if (![nodeIndex containsObject:nodeID]) { nodeIndexToStore = [nodeIndex arrayByAddingObject:nodeID]; diff --git a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m index 4c7375c45dd06b..db8bd300df94e1 100644 --- a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m +++ b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m @@ -2834,7 +2834,7 @@ - (void)testDataStorageUpdatesWhenRemovingAttributes MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:testEndpoint clusterID:cluster]; if ([cluster isEqualToNumber:@(MTRClusterIDTypeIdentifyID)]) { - MTRDeviceClusterData * data = [device _getClusterDataForPath:path]; + MTRDeviceClusterData * data = [device unitTestGetClusterDataForPath:path]; XCTAssertNotNil(data); XCTAssertNotNil(data.attributes); @@ -2897,7 +2897,7 @@ - (void)testDataStorageUpdatesWhenRemovingAttributes MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:testEndpoint clusterID:cluster]; if ([cluster isEqualToNumber:@(MTRClusterIDTypeIdentifyID)]) { - MTRDeviceClusterData * data = [device _getClusterDataForPath:path]; + MTRDeviceClusterData * data = [device unitTestGetClusterDataForPath:path]; XCTAssertNotNil(data); XCTAssertNotNil(data.attributes); @@ -2937,4 +2937,128 @@ - (void)testDataStorageUpdatesWhenRemovingAttributes XCTAssertFalse([controller isRunning]); } +// Run the test here since detectLeaks is set, and subscription reset needs to not cause leaks +- (void)testMTRDeviceResetSubscription +{ + __auto_type * storageDelegate = [[MTRTestPerControllerStorageWithBulkReadWrite alloc] initWithControllerID:[NSUUID UUID]]; + + __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 = @(333); + NSNumber * fabricID = @(444); + + NSError * error; + + 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 = @(22); + 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 * subscriptionExpectation1 = [self expectationWithDescription:@"Subscription has been set up 1"]; + + delegate.onReportEnd = ^{ + [subscriptionExpectation1 fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ subscriptionExpectation1 ] timeout:60]; + + // Test 1: test that subscription reset works + + XCTestExpectation * subscriptionExpectation2 = [self expectationWithDescription:@"Subscription has been set up 2"]; + + __weak __auto_type weakDelegate = delegate; + delegate.onReportEnd = ^{ + [subscriptionExpectation2 fulfill]; + // reset callback so expectation not fulfilled twice + __strong __auto_type strongDelegate = weakDelegate; + strongDelegate.onReportEnd = nil; + }; + + // clear cluster data before reset + NSUInteger attributeCountBeforeReset = [device unitTestAttributeCount]; + [device unitTestClearClusterData]; + + [device unitTestResetSubscription]; + + [self waitForExpectations:@[ subscriptionExpectation2 ] timeout:60]; + + // check that in-memory cache has recovered + NSUInteger attributeCountAfterReset = [device unitTestAttributeCount]; + XCTAssertEqual(attributeCountBeforeReset, attributeCountAfterReset); + + // Test 2: simulate a cache purge and loss of storage, to see: + // * that subscription reestablishes + // * the cache is restored + [device unitTestClearClusterData]; + [controller.controllerDataStore clearAllStoredClusterData]; + + NSDictionary * storedClusterData = [controller.controllerDataStore getStoredClusterDataForNodeID:deviceID]; + XCTAssertEqual(storedClusterData.count, 0); + + XCTestExpectation * subscriptionExpectation3 = [self expectationWithDescription:@"Subscription has been set up 3"]; + delegate.onReportEnd = ^{ + [subscriptionExpectation3 fulfill]; + // reset callback so expectation not fulfilled twice + __strong __auto_type strongDelegate = weakDelegate; + strongDelegate.onReportEnd = nil; + }; + + // now get list of clusters, and call clusterDataForPath: to trigger the reset + NSSet * persistedClusters = [device unitTestGetPersistedClusters]; + MTRDeviceClusterData * data = [device unitTestGetClusterDataForPath:persistedClusters.anyObject]; + XCTAssertNil(data); + + // Also call clusterDataForPath: repeatedly to verify in logs that subscription is reset only once + for (MTRClusterPath * path in persistedClusters) { + MTRDeviceClusterData * data = [device unitTestGetClusterDataForPath:path]; + (void) data; // do not assert nil because subscription may happen during this time and already fill in the cache + } + + [self waitForExpectations:@[ subscriptionExpectation3 ] timeout:60]; + + // Verify that after report ends all the cluster data is back + for (MTRClusterPath * path in persistedClusters) { + MTRDeviceClusterData * data = [device unitTestGetClusterDataForPath:path]; + XCTAssertNotNil(data); + } + + // 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 0705cb89cb1ff6..46d6c61e950f2d 100644 --- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h +++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h @@ -47,8 +47,6 @@ NS_ASSUME_NONNULL_BEGIN @interface MTRDevice (Test) - (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary *)theOther; -- (MTRDeviceClusterData *)_getClusterDataForPath:(MTRClusterPath *)path; -- (BOOL)_clusterHasBeenPersisted:(MTRClusterPath *)path; - (NSMutableArray *)arrayOfNumbersFromAttributeValue:(MTRDeviceDataValueDictionary)dataDictionary; @end @@ -79,6 +77,10 @@ NS_ASSUME_NONNULL_BEGIN deviceReportingExcessivelyIntervalThreshold:(NSTimeInterval)deviceReportingExcessivelyIntervalThreshold; - (void)unitTestSetMostRecentReportTimes:(NSMutableArray *)mostRecentReportTimes; - (NSUInteger)unitTestNonnullDelegateCount; +- (void)unitTestResetSubscription; +- (MTRDeviceClusterData *)unitTestGetClusterDataForPath:(MTRClusterPath *)path; +- (NSSet *)unitTestGetPersistedClusters; +- (BOOL)unitTestClusterHasBeenPersisted:(MTRClusterPath *)path; @end #endif diff --git a/src/python_testing/TC_DA_1_7.py b/src/python_testing/TC_DA_1_7.py index 8338cf4713a2a5..be1e466002eebc 100644 --- a/src/python_testing/TC_DA_1_7.py +++ b/src/python_testing/TC_DA_1_7.py @@ -158,15 +158,13 @@ def steps_TC_DA_1_7(self): @async_test_body async def test_TC_DA_1_7(self): - # post_cert_tests (or sdk) can use the qr or manual code - # We don't currently support this in cert because the base doesn't support multiple QR/manual num = 0 if self.matter_test_config.discriminators: num += len(self.matter_test_config.discriminators) if self.matter_test_config.qr_code_content: - num += 1 + num += len(self.matter_test_config.qr_code_content) if self.matter_test_config.manual_code: - num += 1 + num += len(self.matter_test_config.manual_code) if num != self.expected_number_of_DUTs(): if self.allow_sdk_dac: diff --git a/src/python_testing/TC_SC_7_1.py b/src/python_testing/TC_SC_7_1.py new file mode 100644 index 00000000000000..b702bf438fba86 --- /dev/null +++ b/src/python_testing/TC_SC_7_1.py @@ -0,0 +1,114 @@ +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# 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. +# + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ALL_CLUSTERS_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 2222 --KVS kvs1 --trace-to json:${TRACE_APP}.json +# test-runner-run/run1/script-args: --storage-path admin_storage.json --bool-arg post_cert_test:true --qr-code MT:-24J0KCZ16750648G00 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + +# Note that in the CI we are using the post-cert test as we can only start one app from the current script. +# This should still be fine as this test has unit tests for other conditions. See test_TC_SC_7_1.py +import logging + +import chip.clusters as Clusters +from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts + + +def _trusted_root_test_step(dut_num: int) -> TestStep: + read_trusted_roots_over_pase = f'TH establishes a PASE session to DUT{dut_num} using the provided setup code and reads the TrustedRootCertificates attribute from the operational credentials cluster over PASE' + return TestStep(dut_num, read_trusted_roots_over_pase, "List should be empty as the DUT should be in factory reset ") + + +class TC_SC_7_1(MatterBaseTest): + ''' TC-SC-7.1 + + This test requires two instances of the DUT with the same PID/VID to confirm that the individual + devices are provisioned with different discriminators and PAKE salts in the same product line. + + This test MUST be run on a factory reset device, over PASE, with no commissioned fabrics. + ''' + + def __init__(self, *args): + super().__init__(*args) + self.post_cert_test = False + + def setup_class(self): + super().setup_class() + self.post_cert_test = self.user_params.get("post_cert_test", False) + + def expected_number_of_DUTs(self) -> int: + return 1 if self.post_cert_test else 2 + + def steps_TC_SC_7_1(self): + if self.post_cert_test: + return [_trusted_root_test_step(1), + TestStep(2, "TH extracts the discriminator from the provided setup code", "Ensure the code is not the default")] + + return [_trusted_root_test_step(1), + _trusted_root_test_step(2), + TestStep(3, "TH compares the discriminators from the provided setup codes", "Discriminators do not match")] + + # TODO: Need a pics or something to limit this to devices that have a factory-provided matter setup code (as opposed to a field upgradable device / device with a custom commissioning where this test won't apply) + + @async_test_body + async def test_TC_SC_7_1(self): + # For now, this test is WAY easier if we just ask for the setup code instead of discriminator / passcode + asserts.assert_false(self.matter_test_config.discriminators, + "This test needs to be run with either the QR or manual setup code. The QR code is preferred.") + + if len(self.matter_test_config.qr_code_content + self.matter_test_config.manual_code) != self.expected_number_of_DUTs(): + if self.post_cert_test: + msg = "The post_cert_test flag is only for use post-certification. When using this flag, specify a single discriminator, manual-code or qr-code-content" + else: + msg = "This test requires two devices for use at certification. Specify two device discriminators or QR codes ex. --discriminator 1234 5678" + asserts.fail(msg) + + # Make sure these are no fabrics on the device so we know we're looking at the factory discriminator. This also ensures that the provided codes are correct. + for i, setup_code in enumerate(self.matter_test_config.qr_code_content + self.matter_test_config.manual_code): + self.step(i+1) + await self.default_controller.FindOrEstablishPASESession(setupCode=setup_code, nodeid=i+1) + root_certs = await self.read_single_attribute_check_success(node_id=i+1, cluster=Clusters.OperationalCredentials, attribute=Clusters.OperationalCredentials.Attributes.TrustedRootCertificates, endpoint=0) + asserts.assert_equal( + root_certs, [], "Root certificates found on device. Device must be factory reset before running this test.") + + self.step(i+2) + setup_payload_info = self.get_setup_payload_info() + if self.post_cert_test: + # For post-cert, we're testing against the defaults + # TODO: Does it even make sense to test against a manual code in post-cert? It's such a small space, collisions are likely. Should we restrict post-cert to QR? What if one isn't provided? + asserts.assert_not_equal(setup_payload_info[0].filter_value, 3840, "Device is using the default discriminator") + else: + if setup_payload_info[0].filter_value == setup_payload_info[1].filter_value and self.matter_test_config.manual_code is not None: + logging.warn("The two provided discriminators are the same. Note that this CAN occur by chance, especially when using manual codes with the short discriminator. Consider using a QR code, or a different device if you believe the DUTs have individually provisioned") + asserts.assert_not_equal( + setup_payload_info[0].filter_value, setup_payload_info[1].filter_value, "Devices are using the same discriminator values") + + # TODO: add test for PAKE salt. This needs to be plumbed through starting from HandlePBKDFParamResponse. + # Will handle in a separate follow up as the plumbing here is aggressive and through some of the crypto layers. + # TODO: Other unit-specific values? + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/basic_composition_support.py b/src/python_testing/basic_composition_support.py index 69f9633962eaca..678c249d0abf5d 100644 --- a/src/python_testing/basic_composition_support.py +++ b/src/python_testing/basic_composition_support.py @@ -99,9 +99,11 @@ def ConvertValue(value) -> Any: class BasicCompositionTests: async def connect_over_pase(self, dev_ctrl): - setupCode = self.matter_test_config.qr_code_content if self.matter_test_config.qr_code_content is not None else self.matter_test_config.manual_code - asserts.assert_true(setupCode, "Require either --qr-code or --manual-code.") - await dev_ctrl.FindOrEstablishPASESession(setupCode, self.dut_node_id) + asserts.assert_true(self.matter_test_config.qr_code_content == [] or self.matter_test_config.manual_code == [], + "Cannot have both QR and manual code specified") + setupCode = self.matter_test_config.qr_code_content + self.matter_test_config.manual_code + asserts.assert_equal(len(setupCode), 1, "Require one of either --qr-code or --manual-code.") + await dev_ctrl.FindOrEstablishPASESession(setupCode[0], self.dut_node_id) def dump_wildcard(self, dump_device_composition_path: typing.Optional[str]): node_dump_dict = {endpoint_id: MatterTlvToJson(self.endpoints_tlv[endpoint_id]) for endpoint_id in self.endpoints_tlv} diff --git a/src/python_testing/matter_testing_support.py b/src/python_testing/matter_testing_support.py index 9fab7acf8dd0c0..90ffe5e018ca43 100644 --- a/src/python_testing/matter_testing_support.py +++ b/src/python_testing/matter_testing_support.py @@ -366,8 +366,8 @@ class MatterTestConfig: # This allows cert tests to be run without re-commissioning for RR-1.1. maximize_cert_chains: bool = True - qr_code_content: Optional[str] = None - manual_code: Optional[str] = None + qr_code_content: List[str] = field(default_factory=list) + manual_code: List[str] = field(default_factory=list) wifi_ssid: Optional[str] = None wifi_passphrase: Optional[str] = None @@ -1067,34 +1067,45 @@ def step(self, step: typing.Union[int, str]): self.current_step_index = self.current_step_index + 1 self.step_skipped = False - def get_setup_payload_info(self) -> SetupPayloadInfo: - if self.matter_test_config.qr_code_content is not None: - qr_code = self.matter_test_config.qr_code_content + def get_setup_payload_info(self) -> List[SetupPayloadInfo]: + setup_payloads = [] + for qr_code in self.matter_test_config.qr_code_content: try: - setup_payload = SetupPayload().ParseQrCode(qr_code) + setup_payloads.append(SetupPayload().ParseQrCode(qr_code)) except ChipStackError: asserts.fail(f"QR code '{qr_code} failed to parse properly as a Matter setup code.") - elif self.matter_test_config.manual_code is not None: - manual_code = self.matter_test_config.manual_code + for manual_code in self.matter_test_config.manual_code: try: - setup_payload = SetupPayload().ParseManualPairingCode(manual_code) + setup_payloads.append(SetupPayload().ParseManualPairingCode(manual_code)) except ChipStackError: asserts.fail( f"Manual code code '{manual_code}' failed to parse properly as a Matter setup code. Check that all digits are correct and length is 11 or 21 characters.") - else: - asserts.fail("Require either --qr-code or --manual-code.") - - info = SetupPayloadInfo() - info.passcode = setup_payload.setup_passcode - if setup_payload.short_discriminator is not None: - info.filter_type = discovery.FilterType.SHORT_DISCRIMINATOR - info.filter_value = setup_payload.short_discriminator - else: - info.filter_type = discovery.FilterType.LONG_DISCRIMINATOR - info.filter_value = setup_payload.long_discriminator - return info + infos = [] + for setup_payload in setup_payloads: + info = SetupPayloadInfo() + info.passcode = setup_payload.setup_passcode + if setup_payload.short_discriminator is not None: + info.filter_type = discovery.FilterType.SHORT_DISCRIMINATOR + info.filter_value = setup_payload.short_discriminator + else: + info.filter_type = discovery.FilterType.LONG_DISCRIMINATOR + info.filter_value = setup_payload.long_discriminator + infos.append(info) + + num_passcodes = 0 if self.matter_test_config.setup_passcodes is None else len(self.matter_test_config.setup_passcodes) + num_discriminators = 0 if self.matter_test_config.discriminators is None else len(self.matter_test_config.discriminators) + asserts.assert_equal(num_passcodes, num_discriminators, "Must have same number of discriminators as passcodes") + if self.matter_test_config.discriminators: + for idx, discriminator in enumerate(self.matter_test_config.discriminators): + info = SetupPayloadInfo() + info.passcode = self.matter_test_config.setup_passcodes[idx] + info.filter_type = DiscoveryFilterType.LONG_DISCRIMINATOR + info.filter_value = discriminator + infos.append(info) + + return infos def wait_for_user_input(self, prompt_msg: str, @@ -1293,27 +1304,23 @@ def populate_commissioning_args(args: argparse.Namespace, config: MatterTestConf config.commissioning_method = args.commissioning_method config.commission_only = args.commission_only - # TODO: this should also allow multiple once QR and manual codes are supported. - config.qr_code_content = args.qr_code - if args.manual_code: - config.manual_code = args.manual_code - else: - config.manual_code = None + config.qr_code_content.extend(args.qr_code) + config.manual_code.extend(args.manual_code) if args.commissioning_method is None: return True - if args.discriminators is None and (args.qr_code is None and args.manual_code is None): + if args.discriminators == [] and (args.qr_code == [] and args.manual_code == []): print("error: Missing --discriminator when no --qr-code/--manual-code present!") return False config.discriminators = args.discriminators - if args.passcodes is None and (args.qr_code is None and args.manual_code is None): + if args.passcodes == [] and (args.qr_code == [] and args.manual_code == []): print("error: Missing --passcode when no --qr-code/--manual-code present!") return False config.setup_passcodes = args.passcodes - if args.qr_code is not None and args.manual_code is not None: + if args.qr_code != [] and args.manual_code != []: print("error: Cannot have both --qr-code and --manual-code present!") return False @@ -1321,8 +1328,7 @@ def populate_commissioning_args(args: argparse.Namespace, config: MatterTestConf print("error: supplied number of discriminators does not match number of passcodes") return False - device_descriptors = [config.qr_code_content] if config.qr_code_content is not None else [ - config.manual_code] if config.manual_code is not None else config.discriminators + device_descriptors = config.qr_code_content + config.manual_code + config.discriminators if len(config.dut_node_ids) > len(device_descriptors): print("error: More node IDs provided than discriminators") @@ -1491,9 +1497,9 @@ def parse_matter_test_args(argv: Optional[List[str]] = None) -> MatterTestConfig code_group = parser.add_mutually_exclusive_group(required=False) code_group.add_argument('-q', '--qr-code', type=str, - metavar="QR_CODE", help="QR setup code content (overrides passcode and discriminator)") + metavar="QR_CODE", default=[], help="QR setup code content (overrides passcode and discriminator)", nargs="+") code_group.add_argument('--manual-code', type=str_from_manual_code, - metavar="MANUAL_CODE", help="Manual setup code content (overrides passcode and discriminator)") + metavar="MANUAL_CODE", default=[], help="Manual setup code content (overrides passcode and discriminator)", nargs="+") fabric_group = parser.add_argument_group( title="Fabric selection", description="Fabric selection for single-fabric basic usage, and commissioning") @@ -1567,15 +1573,7 @@ async def _commission_device(self, i) -> bool: dev_ctrl = self.default_controller conf = self.matter_test_config - # TODO: qr code and manual code aren't lists - - if conf.qr_code_content or conf.manual_code: - info = self.get_setup_payload_info() - else: - info = SetupPayloadInfo() - info.passcode = conf.setup_passcodes[i] - info.filter_type = DiscoveryFilterType.LONG_DISCRIMINATOR - info.filter_value = conf.discriminators[i] + info = self.get_setup_payload_info()[i] if conf.commissioning_method == "on-network": try: diff --git a/src/python_testing/post_certification_tests/production_device_checks.py b/src/python_testing/post_certification_tests/production_device_checks.py index 0e8fd617c44110..6988076e6a87af 100644 --- a/src/python_testing/post_certification_tests/production_device_checks.py +++ b/src/python_testing/post_certification_tests/production_device_checks.py @@ -352,9 +352,9 @@ def __init__(self, code: str, code_type: SetupCodeType): self.config = MatterTestConfig(endpoint=0, dut_node_ids=[ 1], global_test_params=global_test_params, storage_path=self.admin_storage) if code_type == SetupCodeType.QR: - self.config.qr_code_content = code + self.config.qr_code_content = [code] else: - self.config.manual_code = code + self.config.manual_code = [code] self.config.paa_trust_store_path = Path(self.paa_path) # Set for DA-1.2, which uses the CD signing certs for verification. This test is now set to use the production CD signing certs from the DCL. self.config.global_test_params['cd_cert_dir'] = tmpdir_cd diff --git a/src/python_testing/test_testing/MockTestRunner.py b/src/python_testing/test_testing/MockTestRunner.py index 5d6592b5a8cd41..ae8d1730b8fda9 100644 --- a/src/python_testing/test_testing/MockTestRunner.py +++ b/src/python_testing/test_testing/MockTestRunner.py @@ -37,9 +37,12 @@ async def __call__(self, *args, **kwargs): class MockTestRunner(): - def __init__(self, filename: str, classname: str, test: str, endpoint: int, pics: dict[str, bool] = {}): - self.config = MatterTestConfig( - tests=[test], endpoint=endpoint, dut_node_ids=[1], pics=pics) + def __init__(self, filename: str, classname: str, test: str, endpoint: int, pics: dict[str, bool] = None): + self.test = test + self.endpoint = endpoint + self.pics = pics + self.set_test_config(MatterTestConfig()) + self.stack = MatterStackState(self.config) self.default_controller = self.stack.certificate_authorities[0].adminList[0].NewController( nodeId=self.config.controller_node_id, @@ -49,9 +52,20 @@ def __init__(self, filename: str, classname: str, test: str, endpoint: int, pics module = importlib.import_module(Path(os.path.basename(filename)).stem) self.test_class = getattr(module, classname) + def set_test_config(self, test_config: MatterTestConfig): + self.config = test_config + self.config.tests = [self.test] + self.config.endpoint = self.endpoint + if not self.config.dut_node_ids: + self.config.dut_node_ids = [1] + if self.pics: + self.config.pics = self.pics + def Shutdown(self): self.stack.Shutdown() def run_test_with_mock_read(self, read_cache: Attribute.AsyncReadTransaction.ReadResponse): self.default_controller.Read = AsyncMock(return_value=read_cache) + # This doesn't need to do anything since we are overriding the read anyway + self.default_controller.FindOrEstablishPASESession = AsyncMock(return_value=None) return run_tests_no_exit(self.test_class, self.config, None, self.default_controller, self.stack) diff --git a/src/python_testing/test_testing/test_TC_SC_7_1.py b/src/python_testing/test_testing/test_TC_SC_7_1.py new file mode 100644 index 00000000000000..3b1b6a5b1cd269 --- /dev/null +++ b/src/python_testing/test_testing/test_TC_SC_7_1.py @@ -0,0 +1,174 @@ +#!/usr/bin/env -S python3 -B +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# 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 os +import sys +from random import randbytes + +import chip.clusters as Clusters +from chip.clusters import Attribute +from MockTestRunner import MockTestRunner + +try: + from matter_testing_support import MatterTestConfig +except ImportError: + sys.path.append(os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) + from matter_testing_support import MatterTestConfig + + +def read_trusted_root(filled: bool) -> Attribute.AsyncReadTransaction.ReadResponse: + opcreds = Clusters.OperationalCredentials + trusted_roots = [randbytes(400)] if filled else [] + resp = Attribute.AsyncReadTransaction.ReadResponse({}, [], {}) + resp.attributes = {0: {opcreds: {opcreds.Attributes.TrustedRootCertificates: trusted_roots}}} + return resp + + +def main(): + # All QR and manual codes use vendor ID 0xFFF1, product ID 0x8000. + qr_2222_20202021 = 'MT:Y.K908OC16750648G00' + qr_3333_20202021 = 'MT:Y.K900C415W80648G00' + qr_2222_20202024 = 'MT:Y.K908OC16N71648G00' + qr_3840_20202021 = 'MT:Y.K90-Q000KA0648G00' + manual_2222_20202021 = '20054912334' + manual_3333_20202021 = '31693312339' + manual_2222_20202024 = '20055212333' + + test_runner = MockTestRunner('TC_SC_7_1', 'TC_SC_7_1', 'test_TC_SC_7_1', 0) + failures = [] + + # Tests with no code specified should fail + test_config = MatterTestConfig() + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on test with no codes') + + # Tests using discriminators should fail because we need QR or manual codes, no matter the number + test_config = MatterTestConfig(discriminators=[2222], setup_passcodes=[20202021]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on test with 1 discriminator') + + test_config = MatterTestConfig(discriminators=[2222, 3333], setup_passcodes=[20202021, 20202021]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on test with 2 discriminators') + + # Single qr code or manual without post-cert should cause a failure + test_config = MatterTestConfig(qr_code_content=[qr_2222_20202021]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on test with 1 QR code') + + test_config = MatterTestConfig(manual_code=[manual_2222_20202021]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on test with 1 manual code') + + # Two QR or manual codes with post cert marked should fail + test_config = MatterTestConfig(qr_code_content=[qr_2222_20202021, qr_3333_20202021], + global_test_params={'post_cert_test': True}) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on post-cert test with 2 QR codes') + + test_config = MatterTestConfig(manual_code=[manual_2222_20202021, manual_3333_20202021], + global_test_params={'post_cert_test': True}) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on post-cert test with 2 manual codes') + + # Incorrectly formatted codes should fail + test_config = MatterTestConfig(manual_code=[qr_2222_20202021, qr_2222_20202024]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on test with incorrectly formatted manual codes') + + test_config = MatterTestConfig(qr_code_content=[manual_2222_20202021, manual_2222_20202024]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on test with incorrectly formatted QR codes') + + # Two codes with the same discriminator should fail + test_config = MatterTestConfig(qr_code_content=[qr_2222_20202021, qr_2222_20202024]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on test with 2 QR codes with the same discriminator') + + test_config = MatterTestConfig(manual_code=[manual_2222_20202021, manual_2222_20202024]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on test with 2 manual codes with the same discriminator') + + # Post cert test should fail on default discriminator + test_config = MatterTestConfig(qr_code_content=[qr_3840_20202021], global_test_params={'post_cert_test': True}) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if ok: + failures.append('Expected assertion on post-cert test with default code') + + # Test should fail if there is fabric info + test_config = MatterTestConfig(qr_code_content=[qr_2222_20202021, qr_3333_20202021]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(True)) + if ok: + failures.append('Expected assertion on test when fabrics are present') + + # Test should pass on codes with two different discriminators + test_config = MatterTestConfig(qr_code_content=[qr_2222_20202021, qr_3333_20202021]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if not ok: + failures.append('Expected pass on QR code test') + + test_config = MatterTestConfig(manual_code=[manual_2222_20202021, manual_3333_20202021]) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if not ok: + failures.append('Expected pass on manual code test') + + # Test should pass on post-cert test + test_config = MatterTestConfig(qr_code_content=[qr_2222_20202021], global_test_params={'post_cert_test': True}) + test_runner.set_test_config(test_config) + ok = test_runner.run_test_with_mock_read(read_trusted_root(False)) + if not ok: + failures.append('Expected pass on post-cert test') + + test_runner.Shutdown() + print( + f"Test of TC-SC-7.1: test response incorrect: {len(failures)}") + for f in failures: + print(f) + + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main())