blob: c0dae34eb9ed7076fd9ebab69e53a6b7e74e4467 [file]
#
# Copyright (c) 2025 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 tempfile
from mobly import asserts
import matter.clusters as Clusters
from matter.clusters.Attribute import AsyncReadTransaction
from matter.testing.global_attribute_ids import GlobalAttributeIds
from matter.testing.matter_testing import CertificationUnitTestNoDevice
from matter.testing.pics import (BASE_PICS_CODES_DERIVED, BasePicsFacts, base_pics_facts_to_pics_codes,
derive_base_pics_facts_from_device_wildcard, generate_device_element_pics_from_device_wildcard,
read_pics_from_file)
from matter.testing.runner import default_matter_test_main
from matter.testing.spec_parsing import PrebuiltDataModelDirectory, build_xml_clusters
class TestPicsHelpers(CertificationUnitTestNoDevice):
def test_pics_generation(self):
xml_cluster, _ = build_xml_clusters(PrebuiltDataModelDirectory.k1_4_1)
# 0: opcreds with an attribute, an accepted command, a generated command
# 0: cadmin with a feature, an attribute, an accepted command
# 1: Door lock cluster with two features, two attributes, two accepted commands, two generated commands
wildcard = AsyncReadTransaction.ReadResponse(attributes={}, events=[], tlvAttributes={})
desc = Clusters.Descriptor
opcreds = Clusters.OperationalCredentials
cadmin = Clusters.AdministratorCommissioning
lock = Clusters.DoorLock
wildcard.tlvAttributes[0] = {desc.id: {}, opcreds.id: {}, cadmin.id: {}}
wildcard.tlvAttributes[1] = {lock.id: {}}
wildcard.tlvAttributes[0][desc.id][desc.Attributes.AttributeList.attribute_id] = [
desc.Attributes.DeviceTypeList.attribute_id]
wildcard.tlvAttributes[0][desc.id][desc.Attributes.AcceptedCommandList.attribute_id] = []
wildcard.tlvAttributes[0][desc.id][desc.Attributes.GeneratedCommandList.attribute_id] = []
wildcard.tlvAttributes[0][desc.id][desc.Attributes.FeatureMap.attribute_id] = 0
desc_expected = ['DESC.S', 'DESC.S.A0000']
# The rest of the processing is done through tlvAttributes, but this one needs to be parsed, so it comes from here.
wildcard.attributes = {0: {desc: {desc.Attributes.DeviceTypeList: [
desc.Structs.DeviceTypeStruct(deviceType=0x16, revision=1)]}}}
wildcard.tlvAttributes[0][opcreds.id][opcreds.Attributes.AttributeList.attribute_id] = [
opcreds.Attributes.NOCs.attribute_id]
wildcard.tlvAttributes[0][opcreds.id][opcreds.Attributes.AcceptedCommandList.attribute_id] = [
opcreds.Commands.AttestationRequest.command_id, opcreds.Commands.AddTrustedRootCertificate.command_id]
wildcard.tlvAttributes[0][opcreds.id][opcreds.Attributes.GeneratedCommandList.attribute_id] = [
opcreds.Commands.AttestationResponse.command_id]
wildcard.tlvAttributes[0][opcreds.id][opcreds.Attributes.FeatureMap.attribute_id] = 0
opcreds_expected_suffix = ['A0000', 'C00.Rsp', 'C0b.Rsp', 'C01.Tx']
opcreds_expected = [f'OPCREDS.S.{s}' for s in opcreds_expected_suffix]
opcreds_expected.append('OPCREDS.S')
wildcard.tlvAttributes[0][cadmin.id][cadmin.Attributes.AttributeList.attribute_id] = [
cadmin.Attributes.AdminFabricIndex.attribute_id]
wildcard.tlvAttributes[0][cadmin.id][cadmin.Attributes.AcceptedCommandList.attribute_id] = [
cadmin.Commands.OpenCommissioningWindow.command_id]
wildcard.tlvAttributes[0][cadmin.id][cadmin.Attributes.GeneratedCommandList.attribute_id] = []
wildcard.tlvAttributes[0][cadmin.id][cadmin.Attributes.FeatureMap.attribute_id] = 1
cadmin_expected_suffix = ['A0001', 'C00.Rsp', 'F00']
cadmin_expected = [f'CADMIN.S.{s}' for s in cadmin_expected_suffix]
cadmin_expected.append('CADMIN.S')
wildcard.tlvAttributes[1][lock.id][lock.Attributes.AttributeList.attribute_id] = [
lock.Attributes.DoorState.attribute_id, lock.Attributes.LockType.attribute_id]
wildcard.tlvAttributes[1][lock.id][lock.Attributes.AcceptedCommandList.attribute_id] = [
lock.Commands.GetUser.command_id, lock.Commands.GetCredentialStatus.command_id]
wildcard.tlvAttributes[1][lock.id][lock.Attributes.GeneratedCommandList.attribute_id] = [
lock.Commands.GetUserResponse.command_id, lock.Commands.GetCredentialStatusResponse.command_id]
wildcard.tlvAttributes[1][lock.id][lock.Attributes.FeatureMap.attribute_id] = 3
lock_expected_suffix = ['A0003', 'A0001', 'C1b.Rsp', 'C24.Rsp', 'C1c.Tx', 'C25.Tx', 'F00', 'F01']
lock_expected = [f'DRLK.S.{s}' for s in lock_expected_suffix]
lock_expected.append('DRLK.S')
def check_expected_pics(pics_list: dict[int, list[str]]):
asserts.assert_equal(set(pics_list.keys()), {0, 1}, "Unexpected endpoints in PICS list")
asserts.assert_equal(set(pics_list[0]), set(desc_expected + opcreds_expected + cadmin_expected +
['IDM.S', 'MCORE.ROLE.COMMISSIONEE']), "Incorrect PICS list on EP0")
asserts.assert_equal(set(pics_list[1]), set(lock_expected + ['IDM.S']), "Incorrect PICS list on EP1")
pics_list, problems = generate_device_element_pics_from_device_wildcard(wildcard, xml_cluster)
asserts.assert_equal(len(problems), 0, "Unexpected problems found generating PICS list")
check_expected_pics(pics_list)
# Add globals, should be no errors, should not appear
wildcard.tlvAttributes[1][lock.id][lock.Attributes.AttributeList.attribute_id].extend(
[lock.Attributes.AttributeList.attribute_id, lock.Attributes.AcceptedCommandList.attribute_id, lock.Attributes.GeneratedCommandList.attribute_id, lock.Attributes.FeatureMap.attribute_id, lock.Attributes.ClusterRevision.attribute_id])
pics_list, problems = generate_device_element_pics_from_device_wildcard(wildcard, xml_cluster)
asserts.assert_equal(len(problems), 0, "Unexpected problems found generating PICS list")
check_expected_pics(pics_list)
# Add MEI cluster, MEI attribute, MEI accepted command, MEI generated command - should be no errors, should not appear
unit_testing = Clusters.UnitTesting
wildcard.tlvAttributes[1][unit_testing.id] = {}
# MEI cluster
wildcard.tlvAttributes[1][unit_testing.id][unit_testing.Attributes.AttributeList.attribute_id] = [
unit_testing.Attributes.Bitmap16.attribute_id]
wildcard.tlvAttributes[1][lock.id][lock.Attributes.AttributeList.attribute_id].append(0x60060000)
wildcard.tlvAttributes[1][lock.id][lock.Attributes.AcceptedCommandList.attribute_id].append(0x60060000)
wildcard.tlvAttributes[1][lock.id][lock.Attributes.GeneratedCommandList.attribute_id].append(0x60060001)
pics_list, problems = generate_device_element_pics_from_device_wildcard(wildcard, xml_cluster)
asserts.assert_equal(len(problems), 0, "Unexpected problems found generating PICS list")
check_expected_pics(pics_list)
# Add a standard cluster that's not part of the standard cluster set - this should cause an error because we don't know the PICS
unknown_standard_cluster = max(xml_cluster.keys()) + 1
wildcard.tlvAttributes[1][unknown_standard_cluster] = {}
wildcard.tlvAttributes[1][unknown_standard_cluster][unit_testing.Attributes.AttributeList.attribute_id] = [0]
pics_list, problems = generate_device_element_pics_from_device_wildcard(wildcard, xml_cluster)
asserts.assert_equal(len(problems), 1, "Unexpected problems found generating PICS list")
check_expected_pics(pics_list)
def test_base_pics_facts_to_pics_codes(self):
# Empty facts must produce an empty code set so a non-commissioned,
# non-server fact bundle does not accidentally mark anything.
asserts.assert_equal(base_pics_facts_to_pics_codes(BasePicsFacts()), set(),
"Empty BasePicsFacts must produce no PICS codes")
# Every individual flag, mapped one at a time, must appear in the
# tracked-codes set so callers can rely on BASE_PICS_CODES_DERIVED.
cases = [
(BasePicsFacts(is_commissionee=True), "MCORE.ROLE.COMMISSIONEE"),
(BasePicsFacts(is_server=True), "MCORE.IDM.S"),
(BasePicsFacts(is_bridge=True), "MCORE.BRIDGE"),
(BasePicsFacts(is_ota_requestor=True), "MCORE.OTA.Requestor"),
(BasePicsFacts(is_ota_provider=True), "MCORE.OTA.Provider"),
(BasePicsFacts(has_groups_on_multiple_endpoints=True), "MCORE.G.MULTIENDPOINT"),
]
for facts, expected_code in cases:
codes = base_pics_facts_to_pics_codes(facts)
asserts.assert_equal(codes, {expected_code}, f"Expected only {expected_code}, got {codes}")
asserts.assert_in(expected_code, BASE_PICS_CODES_DERIVED,
f"{expected_code} must be in BASE_PICS_CODES_DERIVED so the test step iterates it")
# All-on must produce all codes.
all_on = BasePicsFacts(
is_commissionee=True, is_server=True, is_bridge=True,
is_ota_requestor=True, is_ota_provider=True,
has_groups_on_multiple_endpoints=True)
asserts.assert_equal(base_pics_facts_to_pics_codes(all_on), set(BASE_PICS_CODES_DERIVED),
"All-on facts must produce the full tracked-codes set")
def test_derive_base_pics_facts_from_device_wildcard(self):
xml_cluster, _ = build_xml_clusters(PrebuiltDataModelDirectory.k1_4_1)
desc = Clusters.Descriptor
opcreds = Clusters.OperationalCredentials
groups = Clusters.Groups
ota_req = Clusters.OtaSoftwareUpdateRequestor
# Minimal "root node plus bridged child with Groups on EP1 and EP2" wildcard.
# EP0: root node device type, OpCreds (so endpoint has a server),
# OTA Requestor (server-side).
# EP1: Aggregator device type, Groups server.
# EP2: Groups server.
wildcard = AsyncReadTransaction.ReadResponse(attributes={}, events=[], tlvAttributes={})
wildcard.attributes[0] = {
desc: {desc.Attributes.DeviceTypeList: [
desc.Structs.DeviceTypeStruct(deviceType=0x16, revision=1)]},
}
wildcard.attributes[1] = {
desc: {desc.Attributes.DeviceTypeList: [
desc.Structs.DeviceTypeStruct(deviceType=0x000E, revision=1)]},
}
wildcard.attributes[2] = {
desc: {desc.Attributes.DeviceTypeList: []},
}
# Build minimal global-attribute fills for each cluster the helper
# will visit. Empty attribute/command lists are fine for the
# rules covered here; event conformance is covered in its own test.
def _empty_globals(feature_map=0):
return {
GlobalAttributeIds.ATTRIBUTE_LIST_ID: [],
GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID: [],
GlobalAttributeIds.GENERATED_COMMAND_LIST_ID: [],
GlobalAttributeIds.FEATURE_MAP_ID: feature_map,
GlobalAttributeIds.CLUSTER_REVISION_ID: 1,
}
wildcard.tlvAttributes[0] = {
desc.id: _empty_globals(),
opcreds.id: _empty_globals(),
ota_req.id: _empty_globals(),
}
wildcard.tlvAttributes[1] = {
desc.id: _empty_globals(),
groups.id: _empty_globals(),
}
wildcard.tlvAttributes[2] = {
desc.id: _empty_globals(),
groups.id: _empty_globals(),
}
facts, problems = derive_base_pics_facts_from_device_wildcard(wildcard, xml_cluster)
asserts.assert_equal(problems, [], "Unexpected derivation problems")
asserts.assert_true(facts.is_commissionee, "Root node device type on EP0 must derive is_commissionee")
asserts.assert_true(facts.is_server, "Standard server clusters on EP0 must derive is_server")
asserts.assert_true(facts.is_bridge, "Aggregator device type on EP1 must derive is_bridge")
asserts.assert_true(facts.is_ota_requestor, "OTA Requestor cluster on EP0 must derive is_ota_requestor")
asserts.assert_false(facts.is_ota_provider, "OTA Provider not present, must not derive is_ota_provider")
asserts.assert_true(facts.has_groups_on_multiple_endpoints,
"Groups on EP1 + EP2 must derive has_groups_on_multiple_endpoints")
# Translator should map this exactly to the expected MCORE set,
# excluding the one it doesn't cover (OTA Provider).
expected_codes = {
"MCORE.ROLE.COMMISSIONEE", "MCORE.IDM.S", "MCORE.BRIDGE",
"MCORE.OTA.Requestor", "MCORE.G.MULTIENDPOINT",
}
asserts.assert_equal(base_pics_facts_to_pics_codes(facts), expected_codes,
"Translator output drifted from the per-rule facts")
def test_derive_base_pics_facts_groups_on_single_endpoint(self):
# Groups cluster on a single endpoint must NOT derive
# has_groups_on_multiple_endpoints. The >= 2 threshold is the
# whole point of the MCORE.G.MULTIENDPOINT PICS.
xml_cluster, _ = build_xml_clusters(PrebuiltDataModelDirectory.k1_4_1)
desc = Clusters.Descriptor
groups = Clusters.Groups
wildcard = AsyncReadTransaction.ReadResponse(attributes={}, events=[], tlvAttributes={})
wildcard.attributes[0] = {desc: {desc.Attributes.DeviceTypeList: []}}
wildcard.attributes[1] = {desc: {desc.Attributes.DeviceTypeList: []}}
wildcard.tlvAttributes[0] = {
desc.id: {
GlobalAttributeIds.ATTRIBUTE_LIST_ID: [],
GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID: [],
GlobalAttributeIds.GENERATED_COMMAND_LIST_ID: [],
GlobalAttributeIds.FEATURE_MAP_ID: 0,
GlobalAttributeIds.CLUSTER_REVISION_ID: 1,
},
}
wildcard.tlvAttributes[1] = {
desc.id: {
GlobalAttributeIds.ATTRIBUTE_LIST_ID: [],
GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID: [],
GlobalAttributeIds.GENERATED_COMMAND_LIST_ID: [],
GlobalAttributeIds.FEATURE_MAP_ID: 0,
GlobalAttributeIds.CLUSTER_REVISION_ID: 1,
},
groups.id: {
GlobalAttributeIds.ATTRIBUTE_LIST_ID: [],
GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID: [],
GlobalAttributeIds.GENERATED_COMMAND_LIST_ID: [],
GlobalAttributeIds.FEATURE_MAP_ID: 0,
GlobalAttributeIds.CLUSTER_REVISION_ID: 1,
},
}
facts, problems = derive_base_pics_facts_from_device_wildcard(wildcard, xml_cluster)
asserts.assert_equal(problems, [], "Unexpected derivation problems")
asserts.assert_false(facts.has_groups_on_multiple_endpoints,
"Groups on a single endpoint must not derive MCORE.G.MULTIENDPOINT")
def test_derive_base_pics_facts_mandatory_events(self):
# AccessControl on EP0 has spec-MANDATORY events (AccessControlEntryChanged,
# AccessControlExtensionChanged). Their conformance evaluates to
# mandatory with no feature-map dependency, so they should land in
# facts.mandatory_events_by_cluster regardless of feature bits.
xml_cluster, _ = build_xml_clusters(PrebuiltDataModelDirectory.k1_4_1)
acl = Clusters.AccessControl
desc = Clusters.Descriptor
# Only run this rule if the chosen DM XML actually has events for
# AccessControl; otherwise the test would be tautological.
asserts.assert_true(
len(xml_cluster[acl.id].events) > 0,
"Test fixture assumption broken: AccessControl 1.4.1 must have events in DM XML")
wildcard = AsyncReadTransaction.ReadResponse(attributes={}, events=[], tlvAttributes={})
wildcard.attributes[0] = {desc: {desc.Attributes.DeviceTypeList: []}}
wildcard.tlvAttributes[0] = {
desc.id: {
GlobalAttributeIds.ATTRIBUTE_LIST_ID: [],
GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID: [],
GlobalAttributeIds.GENERATED_COMMAND_LIST_ID: [],
GlobalAttributeIds.FEATURE_MAP_ID: 0,
GlobalAttributeIds.CLUSTER_REVISION_ID: 1,
},
acl.id: {
GlobalAttributeIds.ATTRIBUTE_LIST_ID: [],
GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID: [],
GlobalAttributeIds.GENERATED_COMMAND_LIST_ID: [],
GlobalAttributeIds.FEATURE_MAP_ID: 0,
GlobalAttributeIds.CLUSTER_REVISION_ID: 1,
},
}
facts, _ = derive_base_pics_facts_from_device_wildcard(wildcard, xml_cluster)
ep_events = facts.mandatory_events_by_cluster.get(0, {})
acl_mandatory = ep_events.get(acl.id, set())
asserts.assert_true(
len(acl_mandatory) > 0,
"AccessControl spec-mandatory events must be derived for any DUT with AccessControl on EP0")
def test_read_pics_from_file_endpoint_naming_variants(self):
"""Endpoint subdir matching tolerates common naming conventions."""
test_xml = """<?xml version='1.0' encoding='utf-8'?>
<clusterPICS>
<picsItem>
<itemNumber>TEST.S</itemNumber>
<support>true</support>
</picsItem>
</clusterPICS>
"""
for subdir_name in ['endpoint0', 'EP0', 'ep_0', 'Endpoint 0', '0']:
with tempfile.TemporaryDirectory() as d:
ep_dir = os.path.join(d, subdir_name)
os.makedirs(ep_dir)
with open(os.path.join(ep_dir, 'cluster.xml'), 'w') as f:
f.write(test_xml)
pics = read_pics_from_file(d, endpoint=0)
asserts.assert_true(pics.get('TEST.S'), f'Failed for subdir name: {subdir_name}')
if __name__ == "__main__":
default_matter_test_main()