diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index d6c580e643e9ff..97897e8086c941 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -468,7 +468,6 @@ jobs:
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_RVCCLEANM_1_2.py" --script-args "--int-arg PIXIT_ENDPOINT:1 --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --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 --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_RVCRUNM_1_2.py" --script-args "--int-arg PIXIT_ENDPOINT:1 --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --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/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 --app out/linux-x64-lock-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-lock-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_DRLK_2_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values --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 --app out/linux-x64-lock-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-lock-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_DRLK_2_3.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values --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 --app out/linux-x64-lock-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-lock-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_DRLK_2_12.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
diff --git a/src/python_testing/TC_DeviceBasicComposition.py b/src/python_testing/TC_DeviceBasicComposition.py
index 1f5dc38813a975..b333085fa2eef1 100644
--- a/src/python_testing/TC_DeviceBasicComposition.py
+++ b/src/python_testing/TC_DeviceBasicComposition.py
@@ -31,11 +31,8 @@
import chip.clusters.ClusterObjects
import chip.tlv
from chip.clusters.Attribute import ValueDecodeFailure
-from conformance_support import ConformanceDecision, conformance_allowed
-from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, MatterBaseTest,
- async_test_body, default_matter_test_main)
+from matter_testing_support import AttributePathLocation, MatterBaseTest, async_test_body, default_matter_test_main
from mobly import asserts
-from spec_parsing_support import CommandType, build_xml_clusters
def MatterTlvToJson(tlv_data: dict[int, Any]) -> dict[str, Any]:
@@ -873,129 +870,6 @@ def test_DESC_2_2(self):
if problems or root_problems:
self.fail_current_test("Problems with tags lists")
- def test_spec_conformance(self):
- success = True
- # TODO: provisional needs to be an input parameter
- allow_provisional = True
- clusters, problems = build_xml_clusters()
- self.problems = self.problems + problems
- for id in sorted(list(clusters.keys())):
- print(f'{id} 0x{id:02x}: {clusters[id].name}')
- for endpoint_id, endpoint in self.endpoints_tlv.items():
- for cluster_id, cluster in endpoint.items():
- if cluster_id not in clusters.keys():
- if (cluster_id & 0xFFFF_0000) != 0:
- # manufacturer cluster
- continue
- location = ClusterPathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id)
- # TODO: update this from a warning once we have all the data
- self.record_warning(self.get_test_name(), location=location,
- problem='Standard cluster found on device, but is not present in spec data')
- continue
-
- # TODO: switch to use global FEATURE_MAP_ID etc. once the IDM-10.1 change is merged.
- FEATURE_MAP_ID = 0xFFFC
- ATTRIBUTE_LIST_ID = 0xFFFB
- ACCEPTED_COMMAND_ID = 0xFFF9
- GENERATED_COMMAND_ID = 0xFFF8
-
- feature_map = cluster[FEATURE_MAP_ID]
- attribute_list = cluster[ATTRIBUTE_LIST_ID]
- all_command_list = cluster[ACCEPTED_COMMAND_ID] + cluster[GENERATED_COMMAND_ID]
-
- # Feature conformance checking
- feature_masks = [1 << i for i in range(32) if feature_map & (1 << i)]
- for f in feature_masks:
- location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=FEATURE_MAP_ID)
- if f not in clusters[cluster_id].features.keys():
- self.record_error(self.get_test_name(), location=location, problem=f'Unknown feature with mask 0x{f:02x}')
- success = False
- continue
- xml_feature = clusters[cluster_id].features[f]
- conformance_decision = xml_feature.conformance(feature_map, attribute_list, all_command_list)
- if not conformance_allowed(conformance_decision, allow_provisional):
- self.record_error(self.get_test_name(), location=location,
- problem=f'Disallowed feature with mask 0x{f:02x}')
- success = False
- for feature_mask, xml_feature in clusters[cluster_id].features.items():
- conformance_decision = xml_feature.conformance(feature_map, attribute_list, all_command_list)
- if conformance_decision == ConformanceDecision.MANDATORY and feature_mask not in feature_masks:
- self.record_error(self.get_test_name(), location=location,
- problem=f'Required feature with mask 0x{f:02x} is not present in feature map')
- success = False
-
- # Attribute conformance checking
- for attribute_id, attribute in cluster.items():
- location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id)
- if attribute_id not in clusters[cluster_id].attributes.keys():
- # TODO: Consolidate the range checks with IDM-10.1 once that lands
- if attribute_id <= 0x4FFF:
- # manufacturer attribute
- self.record_error(self.get_test_name(), location=location,
- problem='Standard attribute found on device, but not in spec')
- success = False
- continue
- xml_attribute = clusters[cluster_id].attributes[attribute_id]
- conformance_decision = xml_attribute.conformance(feature_map, attribute_list, all_command_list)
- if not conformance_allowed(conformance_decision, allow_provisional):
- location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id)
- self.record_error(self.get_test_name(), location=location,
- problem=f'Attribute 0x{attribute_id:02x} is included, but is disallowed by conformance')
- success = False
- for attribute_id, xml_attribute in clusters[cluster_id].attributes.items():
- conformance_decision = xml_attribute.conformance(feature_map, attribute_list, all_command_list)
- if conformance_decision == ConformanceDecision.MANDATORY and attribute_id not in cluster.keys():
- location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id)
- self.record_error(self.get_test_name(), location=location,
- problem=f'Attribute 0x{attribute_id:02x} is required, but is not present on the DUT')
- success = False
-
- def check_spec_conformance_for_commands(command_type: CommandType) -> bool:
- success = True
- # TODO: once IDM-10.1 lands, use the globals
- global_attribute_id = 0xFFF9 if command_type == CommandType.ACCEPTED else 0xFFF8
- xml_commands_dict = clusters[cluster_id].accepted_commands if command_type == CommandType.ACCEPTED else clusters[cluster_id].generated_commands
- command_list = cluster[global_attribute_id]
- for command_id in command_list:
- location = CommandPathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, command_id=command_id)
- if command_id not in xml_commands_dict:
- # TODO: Consolidate range checks with IDM-10.1 once that lands
- if command_id <= 0xFF:
- # manufacturer command
- continue
- self.record_error(self.get_test_name(), location=location,
- problem='Standard command found on device, but not in spec')
- success = False
- continue
- xml_command = xml_commands_dict[command_id]
- conformance_decision = xml_command.conformance(feature_map, attribute_list, all_command_list)
- if not conformance_allowed(conformance_decision, allow_provisional):
- self.record_error(self.get_test_name(), location=location,
- problem=f'Command 0x{command_id:02x} is included, but disallowed by conformance')
- success = False
- for command_id, xml_command in xml_commands_dict.items():
- conformance_decision = xml_command.conformance(feature_map, attribute_list, all_command_list)
- if conformance_decision == ConformanceDecision.MANDATORY and command_id not in command_list:
- location = CommandPathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, command_id=command_id)
- self.record_error(self.get_test_name(), location=location,
- problem=f'Command 0x{command_id:02x} is required, but is not present on the DUT')
- success = False
- return success
-
- # Command conformance checking
- cmd_success = check_spec_conformance_for_commands(CommandType.ACCEPTED)
- success = False if not cmd_success else success
- cmd_success = check_spec_conformance_for_commands(CommandType.GENERATED)
- success = False if not cmd_success else success
-
- # TODO: Add choice checkers
-
- if not success:
- # TODO: Right now, we have failures in all-cluster, so we can't fail this test and keep it in CI. For now, just log.
- # Issue tracking: #29812
- # self.fail_current_test("Problems with conformance")
- logging.error("Problems found with conformance, this should turn into a test failure once #29812 is resolved")
-
if __name__ == "__main__":
default_matter_test_main()
diff --git a/src/python_testing/TestConformanceSupport.py b/src/python_testing/TestConformanceSupport.py
deleted file mode 100644
index 53f9e885ff9449..00000000000000
--- a/src/python_testing/TestConformanceSupport.py
+++ /dev/null
@@ -1,575 +0,0 @@
-#
-# Copyright (c) 2023 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 xml.etree.ElementTree as ElementTree
-
-from conformance_support import ConformanceDecision, ConformanceParseParameters, parse_callable_from_xml
-from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main
-from mobly import asserts
-
-
-class TestConformanceSupport(MatterBaseTest):
- @async_test_body
- async def setup_class(self):
- super().setup_class()
- # a small feature map
- self.feature_names_to_bits = {'AB': 0x01, 'CD': 0x02}
-
- # none, AB, CD, AB&CD
- self.feature_maps = [0x00, 0x01, 0x02, 0x03]
- self.has_ab = [False, True, False, True]
- self.has_cd = [False, False, True, True]
-
- self.attribute_names_to_values = {'attr1': 0x00, 'attr2': 0x01}
- self.attribute_lists = [[], [0x00], [0x01], [0x00, 0x01]]
- self.has_attr1 = [False, True, False, True]
- self.has_attr2 = [False, False, True, True]
-
- self.command_names_to_values = {'cmd1': 0x00, 'cmd2': 0x01}
- self.cmd_lists = [[], [0x00], [0x01], [0x00, 0x01]]
- self.has_cmd1 = [False, True, False, True]
- self.has_cmd2 = [False, False, True, True]
- self.params = ConformanceParseParameters(
- feature_map=self.feature_names_to_bits, attribute_map=self.attribute_names_to_values, command_map=self.command_names_to_values)
-
- @async_test_body
- async def test_conformance_mandatory(self):
- xml = ''
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for f in self.feature_maps:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
-
- @async_test_body
- async def test_conformance_optional(self):
- xml = ''
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for f in self.feature_maps:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
-
- @async_test_body
- async def test_conformance_disallowed(self):
- xml = ''
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for f in self.feature_maps:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.DISALLOWED)
-
- xml = ''
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for f in self.feature_maps:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.DISALLOWED)
-
- @async_test_body
- async def test_conformance_provisional(self):
- xml = ''
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for f in self.feature_maps:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.PROVISIONAL)
-
- @async_test_body
- async def test_conformance_mandatory_on_condition(self):
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_ab[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- # single attribute mandatory
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, a in enumerate(self.attribute_lists):
- if self.has_attr1[i]:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, a in enumerate(self.attribute_lists):
- if self.has_attr2[i]:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- # test command in optional and in boolean - this is the same as attribute essentially, so testing every permutation is overkill
-
- @async_test_body
- async def test_conformance_optional_on_condition(self):
- # single feature optional
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_ab[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- # single attribute optional
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, a in enumerate(self.attribute_lists):
- if self.has_attr1[i]:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, a in enumerate(self.attribute_lists):
- if self.has_attr2[i]:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- # single command optional
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, c in enumerate(self.cmd_lists):
- if self.has_cmd1[i]:
- asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.NOT_APPLICABLE)
-
- xml = (''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, c in enumerate(self.cmd_lists):
- if self.has_cmd2[i]:
- asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.NOT_APPLICABLE)
-
- @async_test_body
- async def test_conformance_not_term_mandatory(self):
- # single feature not mandatory
- xml = (''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if not self.has_ab[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- xml = (''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if not self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- # single attribute not mandatory
- xml = (''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, a in enumerate(self.attribute_lists):
- if not self.has_attr1[i]:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- xml = (''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, a in enumerate(self.attribute_lists):
- if not self.has_attr2[i]:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- @async_test_body
- async def test_conformance_not_term_optional(self):
- # single feature not optional
- xml = (''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if not self.has_ab[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- xml = (''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if not self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- @async_test_body
- async def test_conformance_and_term(self):
- # and term for features only
- xml = (''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_ab[i] and self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- # and term for attributes only
- xml = (''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, a in enumerate(self.attribute_lists):
- if self.has_attr1[i] and self.has_attr2[i]:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- # and term for feature and attribute
- xml = (''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- for j, a in enumerate(self.attribute_lists):
- if self.has_ab[i] and self.has_attr2[j]:
- asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- @async_test_body
- async def test_conformance_or_term(self):
- # or term feature only
- xml = (''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_ab[i] or self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- # or term attribute only
- xml = (''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, a in enumerate(self.attribute_lists):
- if self.has_attr1[i] or self.has_attr2[i]:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- # or term feature and attribute
- xml = (''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- for j, a in enumerate(self.attribute_lists):
- if self.has_ab[i] or self.has_attr2[j]:
- asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.NOT_APPLICABLE)
-
- @async_test_body
- async def test_conformance_and_term_with_not(self):
- # and term with not
- xml = (''
- ''
- ''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if not self.has_ab[i] and self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- @async_test_body
- async def test_conformance_or_term_with_not(self):
- # or term with not on second feature
- xml = (''
- ''
- ''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_ab[i] or not self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- # not around or term with
- xml = (''
- ''
- ''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if not (self.has_ab[i] or self.has_cd[i]):
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- @async_test_body
- async def test_conformance_and_term_with_three_terms(self):
- # and term with three features
- xml = (''
- ''
- ''
- ''
- ''
- ''
- '')
- self.feature_names_to_bits['EF'] = 0x04
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- # no features
- asserts.assert_equal(xml_callable(0x00, [], []), ConformanceDecision.NOT_APPLICABLE)
- # one feature
- asserts.assert_equal(xml_callable(0x01, [], []), ConformanceDecision.NOT_APPLICABLE)
- # all features
- asserts.assert_equal(xml_callable(0x07, [], []), ConformanceDecision.OPTIONAL)
-
- # and term with one of each
- xml = (''
- ''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- for j, a in enumerate(self.attribute_lists):
- for k, c in enumerate(self.cmd_lists):
- if self.has_ab[i] and self.has_attr1[j] and self.has_cmd1[k]:
- asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.NOT_APPLICABLE)
-
- @async_test_body
- async def test_conformance_or_term_with_three_terms(self):
- # or term with three features
- xml = (''
- ''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- # no features
- asserts.assert_equal(xml_callable(0x00, [], []), ConformanceDecision.NOT_APPLICABLE)
- # one feature
- asserts.assert_equal(xml_callable(0x01, [], []), ConformanceDecision.OPTIONAL)
- # all features
- asserts.assert_equal(xml_callable(0x07, [], []), ConformanceDecision.OPTIONAL)
-
- # or term with one of each
- xml = (''
- ''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- for j, a in enumerate(self.attribute_lists):
- for k, c in enumerate(self.cmd_lists):
- if self.has_ab[i] or self.has_attr1[j] or self.has_cmd1[k]:
- asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.NOT_APPLICABLE)
-
- def test_conformance_otherwise(self):
- # AB, O
- xml = (''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_ab[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
-
- # AB, [CD]
- xml = (''
- ''
- ''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_ab[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- elif self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
-
- # AB & !CD, P
- xml = (''
- ''
- ''
- ''
- ''
- ''
- ''
- ''
- ''
- ''
- '')
- et = ElementTree.fromstring(xml)
- xml_callable = parse_callable_from_xml(et, self.params)
- for i, f in enumerate(self.feature_maps):
- if self.has_ab[i] and not self.has_cd[i]:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
- else:
- asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.PROVISIONAL)
-
-
-if __name__ == "__main__":
- default_matter_test_main()
diff --git a/src/python_testing/conformance_support.py b/src/python_testing/conformance_support.py
deleted file mode 100644
index 9df852f10df696..00000000000000
--- a/src/python_testing/conformance_support.py
+++ /dev/null
@@ -1,258 +0,0 @@
-#
-# Copyright (c) 2023 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 xml.etree.ElementTree as ElementTree
-from dataclasses import dataclass
-from enum import Enum, auto
-from typing import Callable
-
-from chip.tlv import uint
-
-OTHERWISE_CONFORM = 'otherwiseConform'
-OPTIONAL_CONFORM = 'optionalConform'
-PROVISIONAL_CONFORM = 'provisionalConform'
-MANDATORY_CONFORM = 'mandatoryConform'
-DEPRECATE_CONFORM = 'deprecateConform'
-DISALLOW_CONFORM = 'disallowConform'
-AND_TERM = 'andTerm'
-OR_TERM = 'orTerm'
-NOT_TERM = 'notTerm'
-FEATURE_TAG = 'feature'
-ATTRIBUTE_TAG = 'attribute'
-COMMAND_TAG = 'command'
-
-
-class ConformanceException(Exception):
- def __init__(self, msg):
- self.msg = msg
-
- def __str__(self):
- return f"ConformanceException({self.msg})"
-
-
-class ConformanceDecision(Enum):
- MANDATORY = auto()
- OPTIONAL = auto()
- NOT_APPLICABLE = auto()
- DISALLOWED = auto()
- PROVISIONAL = auto()
-
-
-@dataclass
-class ConformanceParseParameters:
- feature_map: dict[str, uint]
- attribute_map: dict[str, uint]
- command_map: dict[str, uint]
-
-
-def conformance_allowed(conformance_decision: ConformanceDecision, allow_provisional: bool):
- if conformance_decision == ConformanceDecision.NOT_APPLICABLE or conformance_decision == ConformanceDecision.DISALLOWED:
- return False
- if conformance_decision == ConformanceDecision.PROVISIONAL:
- return allow_provisional
- return True
-
-
-def mandatory(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- return ConformanceDecision.MANDATORY
-
-
-def optional(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- return ConformanceDecision.OPTIONAL
-
-
-def deprecated(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- return ConformanceDecision.DISALLOWED
-
-
-def disallowed(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- return ConformanceDecision.DISALLOWED
-
-
-def provisional(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- return ConformanceDecision.PROVISIONAL
-
-
-def feature(requiredFeature: uint) -> Callable:
- def feature_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- if requiredFeature & feature_map != 0:
- return ConformanceDecision.MANDATORY
- return ConformanceDecision.NOT_APPLICABLE
- return feature_inner
-
-
-def attribute(requiredAttribute: uint) -> Callable:
- def attribute_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- if requiredAttribute in attribute_list:
- return ConformanceDecision.MANDATORY
- return ConformanceDecision.NOT_APPLICABLE
- return attribute_inner
-
-
-def command(requiredCommand: uint) -> Callable:
- def command_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- if requiredCommand in all_command_list:
- return ConformanceDecision.MANDATORY
- return ConformanceDecision.NOT_APPLICABLE
- return command_inner
-
-
-def optional_wrapper(op: Callable) -> Callable:
- def optional_wrapper_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- decision = op(feature_map, attribute_list, all_command_list)
- if decision == ConformanceDecision.MANDATORY or decision == ConformanceDecision.OPTIONAL:
- return ConformanceDecision.OPTIONAL
- elif decision == ConformanceDecision.NOT_APPLICABLE:
- return ConformanceDecision.NOT_APPLICABLE
- else:
- raise ConformanceException(f'Optional wrapping invalid op {decision}')
- return optional_wrapper_inner
-
-
-def mandatory_wrapper(op: Callable) -> Callable:
- def mandatory_wrapper_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- return op(feature_map, attribute_list, all_command_list)
- return mandatory_wrapper_inner
-
-
-def not_operation(op: Callable):
- def not_operation_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- # not operations can't be used with anything that returns DISALLOWED
- # not operations also can't be used with things that are optional
- # ie, ![AB] doesn't make sense, nor does !O
- decision = op(feature_map, attribute_list, all_command_list)
- if decision == ConformanceDecision.OPTIONAL or decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL:
- raise ConformanceException('NOT operation on optional or disallowed item')
- elif decision == ConformanceDecision.NOT_APPLICABLE:
- return ConformanceDecision.MANDATORY
- elif decision == ConformanceDecision.MANDATORY:
- return ConformanceDecision.NOT_APPLICABLE
- else:
- raise ConformanceException('NOT called on item with non-conformance value')
- return not_operation_inner
-
-
-def and_operation(op_list: list[Callable]) -> Callable:
- def and_operation_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- for op in op_list:
- decision = op(feature_map, attribute_list, all_command_list)
- # and operations can't happen on optional or disallowed
- if decision == ConformanceDecision.OPTIONAL or decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL:
- raise ConformanceException('AND operation on optional or disallowed item')
- elif decision == ConformanceDecision.NOT_APPLICABLE:
- return ConformanceDecision.NOT_APPLICABLE
- elif decision == ConformanceDecision.MANDATORY:
- continue
- else:
- raise ConformanceException('Oplist item returned non-conformance value')
- return ConformanceDecision.MANDATORY
- return and_operation_inner
-
-
-def or_operation(op_list: list[Callable]) -> Callable:
- def or_operation_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- for op in op_list:
- decision = op(feature_map, attribute_list, all_command_list)
- if decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL:
- raise ConformanceException('OR operation on optional or disallowed item')
- elif decision == ConformanceDecision.NOT_APPLICABLE:
- continue
- elif decision == ConformanceDecision.MANDATORY:
- return ConformanceDecision.MANDATORY
- elif decision == ConformanceDecision.OPTIONAL:
- return ConformanceDecision.OPTIONAL
- else:
- raise ConformanceException('Oplist item returned non-conformance value')
- return ConformanceDecision.NOT_APPLICABLE
- return or_operation_inner
-
-# TODO: add xor operation once it's required
-# TODO: how would equal and unequal operations work here?
-
-
-def otherwise(op_list: list[Callable]) -> Callable:
- def otherwise_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
- # Otherwise operations apply from left to right. If any of them
- # has a definite decision (optional, mandatory or disallowed), that is the one that applies
- # Provisional items are meant to be marked as the first item in the list
- # Deprecated items are either on their own, or follow an O as O,D.
- # For O,D, optional applies (leftmost), but we should consider some way to warn here as well,
- # possibly in another function
- for op in op_list:
- decision = op(feature_map, attribute_list, all_command_list)
- if decision == ConformanceDecision.NOT_APPLICABLE:
- continue
- return decision
- return ConformanceDecision.NOT_APPLICABLE
- return otherwise_inner
-
-
-def parse_callable_from_xml(element: ElementTree.Element, params: ConformanceParseParameters) -> Callable:
- if len(list(element)) == 0:
- # no subchildren here, so this can only be mandatory, optional, provisional, deprecated, disallowed, feature or attribute
- if element.tag == MANDATORY_CONFORM:
- return mandatory
- elif element.tag == OPTIONAL_CONFORM:
- return optional
- elif element.tag == PROVISIONAL_CONFORM:
- return provisional
- elif element.tag == DEPRECATE_CONFORM:
- return deprecated
- elif element.tag == DISALLOW_CONFORM:
- return disallowed
- elif element.tag == FEATURE_TAG:
- return feature(params.feature_map[element.get('name')])
- elif element.tag == ATTRIBUTE_TAG:
- # Some command conformance tags are marked as attribute, so if this key isn't in attribute, try command
- name = element.get('name')
- if name in params.attribute_map:
- return attribute(params.attribute_map[name])
- else:
- return command(params.command_map[name])
- elif element.tag == COMMAND_TAG:
- return command(params.command_map[element.get('name')])
- else:
- raise ConformanceException(
- f'Unexpected xml conformance element with no children {str(element.tag)} {str(element.attrib)}')
-
- # First build the list, then create the callable for this element
- ops = []
- for sub in element:
- ops.append(parse_callable_from_xml(sub, params))
-
- # optional can be a wrapper as well as a standalone
- # This can be any of the boolean operations, optional or otherwise
- if element.tag == OPTIONAL_CONFORM:
- if len(ops) > 1:
- raise ConformanceException(f'OPTIONAL term found with more than one subelement {list(element)}')
- return optional_wrapper(ops[0])
- elif element.tag == MANDATORY_CONFORM:
- if len(ops) > 1:
- raise ConformanceException(f'MANDATORY term found with more than one subelement {list(element)}')
- return mandatory_wrapper(ops[0])
- elif element.tag == AND_TERM:
- return and_operation(ops)
- elif element.tag == OR_TERM:
- return or_operation(ops)
- elif element.tag == NOT_TERM:
- if len(ops) > 1:
- raise ConformanceException(f'NOT term found with more than one subelement {list(element)}')
- return not_operation(ops[0])
- elif element.tag == OTHERWISE_CONFORM:
- return otherwise(ops)
- else:
- raise ConformanceException(f'Unexpected conformance tag with children {element}')
diff --git a/src/python_testing/matter_testing_support.py b/src/python_testing/matter_testing_support.py
index 398a01f6cd9d17..a394952445de60 100644
--- a/src/python_testing/matter_testing_support.py
+++ b/src/python_testing/matter_testing_support.py
@@ -333,19 +333,6 @@ class CommandPathLocation:
cluster_id: int
command_id: int
-
-@dataclass
-class ClusterPathLocation:
- endpoint_id: int
- cluster_id: int
-
-
-@dataclass
-class FeaturePathLocation:
- endpoint_id: int
- cluster_id: int
- feature_code: str
-
# ProblemSeverity is not using StrEnum, but rather Enum, since StrEnum only
# appeared in 3.11. To make it JSON serializable easily, multiple inheritance
# from `str` is used. See https://stackoverflow.com/a/51976841.
@@ -360,7 +347,7 @@ class ProblemSeverity(str, Enum):
@dataclass
class ProblemNotice:
test_name: str
- location: Union[AttributePathLocation, EventPathLocation, CommandPathLocation, ClusterPathLocation, FeaturePathLocation]
+ location: Union[AttributePathLocation, EventPathLocation, CommandPathLocation]
severity: ProblemSeverity
problem: str
spec_location: str = ""
@@ -564,13 +551,13 @@ async def send_single_cmd(
def print_step(self, stepnum: typing.Union[int, str], title: str) -> None:
logging.info(f'***** Test Step {stepnum} : {title}')
- def record_error(self, test_name: str, location: Union[AttributePathLocation, EventPathLocation, CommandPathLocation, ClusterPathLocation, FeaturePathLocation], problem: str, spec_location: str = ""):
+ def record_error(self, test_name: str, location: Union[AttributePathLocation, EventPathLocation, CommandPathLocation], problem: str, spec_location: str = ""):
self.problems.append(ProblemNotice(test_name, location, ProblemSeverity.ERROR, problem, spec_location))
- def record_warning(self, test_name: str, location: Union[AttributePathLocation, EventPathLocation, CommandPathLocation, ClusterPathLocation, FeaturePathLocation], problem: str, spec_location: str = ""):
+ def record_warning(self, test_name: str, location: Union[AttributePathLocation, EventPathLocation, CommandPathLocation], problem: str, spec_location: str = ""):
self.problems.append(ProblemNotice(test_name, location, ProblemSeverity.WARNING, problem, spec_location))
- def record_note(self, test_name: str, location: Union[AttributePathLocation, EventPathLocation, CommandPathLocation, ClusterPathLocation, FeaturePathLocation], problem: str, spec_location: str = ""):
+ def record_note(self, test_name: str, location: Union[AttributePathLocation, EventPathLocation, CommandPathLocation], problem: str, spec_location: str = ""):
self.problems.append(ProblemNotice(test_name, location, ProblemSeverity.NOTE, problem, spec_location))
def get_setup_payload_info(self) -> SetupPayloadInfo:
diff --git a/src/python_testing/spec_parsing_support.py b/src/python_testing/spec_parsing_support.py
deleted file mode 100644
index b86387ce23073f..00000000000000
--- a/src/python_testing/spec_parsing_support.py
+++ /dev/null
@@ -1,322 +0,0 @@
-#
-# Copyright (c) 2023 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 glob
-import os
-import xml.etree.ElementTree as ElementTree
-from copy import deepcopy
-from dataclasses import dataclass
-from enum import Enum, auto
-from typing import Callable
-
-from chip.tlv import uint
-from conformance_support import (DEPRECATE_CONFORM, DISALLOW_CONFORM, MANDATORY_CONFORM, OPTIONAL_CONFORM, OTHERWISE_CONFORM,
- PROVISIONAL_CONFORM, ConformanceDecision, ConformanceParseParameters, or_operation,
- parse_callable_from_xml)
-from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, EventPathLocation,
- FeaturePathLocation, ProblemNotice, ProblemSeverity)
-
-
-@dataclass
-class XmlFeature:
- code: str
- name: str
- conformance: Callable[[uint], ConformanceDecision]
-
-
-@dataclass
-class XmlAttribute:
- name: str
- datatype: str
- conformance: Callable[[uint], ConformanceDecision]
-
-
-@dataclass
-class XmlCommand:
- name: str
- conformance: Callable[[uint], ConformanceDecision]
-
-
-@dataclass
-class XmlEvent:
- name: str
- conformance: Callable[[uint], ConformanceDecision]
-
-
-@dataclass
-class XmlCluster:
- name: str
- revision: int
- derived: str
- feature_map: dict[str, uint]
- attribute_map: dict[str, uint]
- command_map: dict[str, uint]
- features: dict[str, XmlFeature]
- attributes: dict[uint, XmlAttribute]
- accepted_commands: dict[uint, XmlCommand]
- generated_commands: dict[uint, XmlCommand]
- events: dict[uint, XmlEvent]
-
-
-class CommandType(Enum):
- ACCEPTED = auto()
- GENERATED = auto()
-
-
-def has_zigbee_conformance(conformance: ElementTree.Element) -> bool:
- # For clusters, things with zigbee conformance can share IDs with the matter elements, so we don't want them
-
- # TODO: it's actually possible for a thing to have a zigbee conformance AND to have other conformances, and we should check
- # for that, but for now, this is fine because that hasn't happened in the cluster conformances YET.
- # It does happen for device types, so we need to be careful there.
- condition = conformance.iter('condition')
- for c in condition:
- try:
- c.attrib['name'].lower() == "zigbee"
- return True
- except KeyError:
- continue
- return False
-
-
-class ClusterParser:
- def __init__(self, cluster, cluster_id, name):
- self._problems: list[ProblemNotice] = []
- self._cluster = cluster
- self._cluster_id = cluster_id
- self._name = name
-
- self._derived = None
- try:
- classification = next(cluster.iter('classification'))
- hierarchy = classification.attrib['hierarchy']
- if hierarchy.lower() == 'derived':
- self._derived = classification.attrib['baseCluster']
- except (KeyError, StopIteration):
- self._derived = None
-
- self.feature_elements = self.get_all_feature_elements()
- self.attribute_elements = self.get_all_attribute_elements()
- self.command_elements = self.get_all_command_elements()
- self.event_elements = self.get_all_event_elements()
- self.params = ConformanceParseParameters(feature_map=self.create_feature_map(), attribute_map=self.create_attribute_map(),
- command_map=self.create_command_map())
-
- def get_conformance(self, element: ElementTree.Element) -> ElementTree.Element:
- for sub in element:
- if sub.tag == OTHERWISE_CONFORM or sub.tag == MANDATORY_CONFORM or sub.tag == OPTIONAL_CONFORM or sub.tag == PROVISIONAL_CONFORM or sub.tag == DEPRECATE_CONFORM or sub.tag == DISALLOW_CONFORM:
- return sub
-
- # Conformance is missing, so let's record the problem and treat it as optional for lack of a better choice
- if element.tag == 'feature':
- location = FeaturePathLocation(endpoint_id=0, cluster_id=self._cluster_id, feature_code=element.attrib['code'])
- elif element.tag == 'command':
- location = CommandPathLocation(endpoint_id=0, cluster_id=self._cluster_id, command_id=element.attrib['id'])
- elif element.tag == 'attribute':
- location = AttributePathLocation(endpoint_id=0, cluster_id=self._cluster_id, attribute_id=element.attrib['id'])
- elif element.tag == 'event':
- location = EventPathLocation(endpoint_id=0, cluster_id=self._cluster_id, event_id=element.attrib['id'])
- else:
- location = ClusterPathLocation(endpoing_id=0, cluster_id=self._cluster_id)
- self._problems.append(ProblemNotice(test_name='Spec XML parsing', location=location,
- severity=ProblemSeverity.WARNING, problem='Unable to find conformance element'))
-
- return ElementTree.Element(OPTIONAL_CONFORM)
-
- def get_all_type(self, type_container: str, type_name: str, key_name: str) -> list[tuple[ElementTree.Element, ElementTree.Element]]:
- ret = []
- container_tags = self._cluster.iter(type_container)
- for container in container_tags:
- elements = container.iter(type_name)
- for element in elements:
- try:
- element.attrib[key_name]
- except KeyError:
- # This is a conformance tag, which uses the same name
- continue
- conformance = self.get_conformance(element)
- if has_zigbee_conformance(conformance):
- continue
- ret.append((element, conformance))
- return ret
-
- def get_all_feature_elements(self) -> list[tuple[ElementTree.Element, ElementTree.Element]]:
- ''' Returns a list of features and their conformances'''
- return self.get_all_type('features', 'feature', 'code')
-
- def get_all_attribute_elements(self) -> list[tuple[ElementTree.Element, ElementTree.Element]]:
- ''' Returns a list of attributes and their conformances'''
- return self.get_all_type('attributes', 'attribute', 'id')
-
- def get_all_command_elements(self) -> list[tuple[ElementTree.Element, ElementTree.Element]]:
- ''' Returns a list of commands and their conformances '''
- return self.get_all_type('commands', 'command', 'id')
-
- def get_all_event_elements(self) -> list[tuple[ElementTree.Element, ElementTree.Element]]:
- ''' Returns a list of events and their conformances'''
- return self.get_all_type('events', 'event', 'id')
-
- def create_feature_map(self) -> dict[str, uint]:
- features = {}
- for element, conformance in self.feature_elements:
- features[element.attrib['code']] = 1 << int(element.attrib['bit'], 0)
- return features
-
- def create_attribute_map(self) -> dict[str, uint]:
- attributes = {}
- for element, conformance in self.attribute_elements:
- attributes[element.attrib['name']] = int(element.attrib['id'], 0)
- return attributes
-
- def create_command_map(self) -> dict[str, uint]:
- commands = {}
- for element, conformance in self.command_elements:
- commands[element.attrib['name']] = int(element.attrib['id'], 0)
- return commands
-
- def parse_features(self) -> dict[uint, XmlFeature]:
- features = {}
- for element, conformance in self.feature_elements:
- mask = 1 << int(element.attrib['bit'], 0)
- features[mask] = XmlFeature(code=element.attrib['code'], name=element.attrib['name'],
- conformance=parse_callable_from_xml(conformance, self.params))
- return features
-
- def parse_attributes(self) -> dict[uint, XmlAttribute]:
- attributes = {}
- for element, conformance_xml in self.attribute_elements:
- code = int(element.attrib['id'], 0)
- # Some deprecated attributes don't have their types included, for now, lets just fallback to UNKNOWN
- try:
- datatype = element.attrib['type']
- except KeyError:
- datatype = 'UNKNOWN'
- conformance = parse_callable_from_xml(conformance_xml, self.params)
- if code in attributes:
- # This is one of those fun ones where two different rows have the same id and name, but differ in conformance and ranges
- # I don't have a good way to relate the ranges to the conformance, but they're both acceptable, so let's just or them.
- conformance = or_operation([conformance, attributes[code].conformance])
- attributes[code] = XmlAttribute(name=element.attrib['name'], datatype=datatype,
- conformance=conformance)
- return attributes
-
- def parse_commands(self, command_type: CommandType) -> dict[uint, XmlAttribute]:
- commands = {}
- for element, conformance_xml in self.command_elements:
- code = int(element.attrib['id'], 0)
- dir = CommandType.ACCEPTED
- try:
- if element.attrib['direction'].lower() == 'responsefromserver':
- dir = CommandType.GENERATED
- except KeyError:
- pass
- if dir != command_type:
- continue
- code = int(element.attrib['id'], 0)
- conformance = parse_callable_from_xml(conformance_xml, self.params)
- if code in commands:
- conformance = or_operation([conformance, commands[code].conformance])
- commands[code] = XmlCommand(name=element.attrib['name'], conformance=conformance)
- return commands
-
- def parse_events(self) -> dict[uint, XmlAttribute]:
- events = {}
- for element, conformance_xml in self.event_elements:
- code = int(element.attrib['id'], 0)
- conformance = parse_callable_from_xml(conformance_xml, self.params)
- if code in events:
- conformance = or_operation([conformance, events[code].conformance])
- events[code] = XmlEvent(name=element.attrib['name'], conformance=conformance)
- return events
-
- def create_cluster(self) -> XmlCluster:
- return XmlCluster(revision=self._cluster.attrib['revision'], derived=self._derived,
- name=self._name, feature_map=self.params.feature_map,
- attribute_map=self.params.attribute_map, command_map=self.params.command_map,
- features=self.parse_features(),
- attributes=self.parse_attributes(),
- accepted_commands=self.parse_commands(CommandType.ACCEPTED),
- generated_commands=self.parse_commands(CommandType.GENERATED),
- events=self.parse_events())
-
- def get_problems(self) -> list[ProblemNotice]:
- return self._problems
-
-
-def build_xml_clusters() -> tuple[list[XmlCluster], list[ProblemNotice]]:
- dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'data_model', 'clusters')
- clusters: dict[int, XmlCluster] = {}
- derived_clusters: dict[str, XmlCluster] = {}
- ids_by_name = {}
- problems = []
- for xml in glob.glob(f"{dir}/*.xml"):
- tree = ElementTree.parse(f'{xml}')
- root = tree.getroot()
- cluster = root.iter('cluster')
- for c in cluster:
- name = c.attrib['name']
- if not c.attrib['id']:
- # Fully derived clusters have no id, but also shouldn't appear on a device.
- # We do need to keep them, though, because we need to update the derived
- # clusters. We keep them in a special dict by name, so they can be thrown
- # away later.
- cluster_id = None
- else:
- cluster_id = int(c.attrib['id'], 0)
- ids_by_name[name] = cluster_id
-
- parser = ClusterParser(c, cluster_id, name)
- new = parser.create_cluster()
- problems = problems + parser.get_problems()
-
- if cluster_id:
- clusters[cluster_id] = new
- else:
- derived_clusters[name] = new
-
- # We have the information now about which clusters are derived, so we need to fix them up. Apply first the base cluster,
- # then add the specific cluster overtop
- for id, c in clusters.items():
- if c.derived:
- base_name = c.derived
- if base_name in ids_by_name:
- base = clusters[ids_by_name[c.derived]]
- else:
- base = derived_clusters[base_name]
-
- feature_map = deepcopy(base.feature_map)
- feature_map.update(c.feature_map)
- attribute_map = deepcopy(base.attribute_map)
- attribute_map.update(c.attribute_map)
- command_map = deepcopy(base.command_map)
- command_map.update(c.command_map)
- features = deepcopy(base.features)
- features.update(c.features)
- attributes = deepcopy(base.attributes)
- attributes.update(c.attributes)
- accepted_commands = deepcopy(base.accepted_commands)
- accepted_commands.update(c.accepted_commands)
- generated_commands = deepcopy(base.generated_commands)
- generated_commands.update(c.generated_commands)
- events = deepcopy(base.events)
- events.update(c.events)
- new = XmlCluster(revision=c.revision, derived=c.derived, name=c.name,
- feature_map=feature_map, attribute_map=attribute_map, command_map=command_map,
- features=features, attributes=attributes, accepted_commands=accepted_commands,
- generated_commands=generated_commands, events=events)
- clusters[id] = new
- return clusters, problems