Skip to content

Commit

Permalink
Device type DM XML parser (#33845)
Browse files Browse the repository at this point in the history
* Device type DM XML parser

* Restyled by autopep8

* linter

* Update src/python_testing/TestSpecParsingDeviceType.py

* Apply suggestions from code review

* address review comments

* Restyled by autopep8

* use new directory structure for device types too

* python 3.9 compliance

---------

Co-authored-by: Restyled.io <[email protected]>
  • Loading branch information
cecille and restyled-commits authored Jul 18, 2024
1 parent b88ac27 commit ded3be7
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 62 deletions.
1 change: 1 addition & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ jobs:
scripts/run_in_python_env.sh out/venv './scripts/tests/TestTimeSyncTrustedTimeSourceRunner.py'
scripts/run_in_python_env.sh out/venv './src/python_testing/test_testing/test_TC_ICDM_2_1.py'
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestIdChecks.py'
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'
Expand Down
111 changes: 111 additions & 0 deletions src/python_testing/TestSpecParsingDeviceType.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#
# 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 xml.etree.ElementTree as ElementTree

from jinja2 import Template
from matter_testing_support import MatterBaseTest, default_matter_test_main
from mobly import asserts
from spec_parsing_support import build_xml_device_types, parse_single_device_type


class TestSpecParsingDeviceType(MatterBaseTest):

# This just tests that the current spec can be parsed without failures
def test_spec_device_parsing(self):
device_types, problems = build_xml_device_types()
self.problems += problems
for id, d in device_types.items():
print(str(d))

def setup_class(self):
self.device_type_id = 0xBBEF
self.revision = 2
self.classification_class = "simple"
self.classification_scope = "endpoint"
self.clusters = {0x0003: "Identify", 0x0004: "Groups"}

# Conformance support tests the different types of conformance for clusters, so here we just want to ensure that we're correctly parsing the XML into python
# adds the same attributes and features to every cluster. This is fine for testing.
self.template = Template("""<deviceType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="types types.xsd devicetype devicetype.xsd" id="{{ device_type_id }}" name="Test Device Type" revision="{{ revision }}">
<revisionHistory>
{% for i in range(revision) %}
<revision revision="{{ i }}" summary="Rev"/>
{% endfor %}
</revisionHistory>
<classification {% if classification_class %} class="{{ classification_class }}" {% endif %} {% if classification_scope %} scope="{{ classification_scope }}" {% endif %}/>
<conditions/>
<clusters>
{% for k,v in clusters.items() %}
<cluster id="{{ k }}" name="{{ v }}" side="server">
<mandatoryConform/>
</cluster>
{% endfor %}
</clusters>
</deviceType>""")

def test_device_type_clusters(self):
xml = self.template.render(device_type_id=self.device_type_id, revision=self.revision, classification_class=self.classification_class,
classification_scope=self.classification_scope, clusters=self.clusters)
et = ElementTree.fromstring(xml)
device_type, problems = parse_single_device_type(et)
asserts.assert_equal(len(problems), 0, "Unexpected problems parsing device type conformance")
asserts.assert_equal(len(device_type.keys()), 1, "Unexpected number of device types returned")
asserts.assert_true(self.device_type_id in device_type.keys(), "device type id not found in returned data")
asserts.assert_equal(device_type[self.device_type_id].revision, self.revision, "Unexpected revision")
asserts.assert_equal(len(device_type[self.device_type_id].server_clusters),
len(self.clusters), "Unexpected number of clusters")
for id, name in self.clusters.items():
asserts.assert_equal(device_type[self.device_type_id].server_clusters[id].name, name, "Incorrect cluster name")
asserts.assert_equal(str(device_type[self.device_type_id].server_clusters[id].conformance),
'M', 'Incorrect cluster conformance')

def test_no_clusters(self):
clusters = {}
xml = self.template.render(device_type_id=self.device_type_id, revision=self.revision, classification_class=self.classification_class,
classification_scope=self.classification_scope, clusters=clusters)
et = ElementTree.fromstring(xml)
device_type, problems = parse_single_device_type(et)
asserts.assert_equal(len(problems), 0, "Unexpected problems parsing device type conformance")
asserts.assert_equal(len(device_type.keys()), 1, "Unexpected number of device types returned")
asserts.assert_true(self.device_type_id in device_type.keys(), "device type id not found in returned data")
asserts.assert_equal(device_type[self.device_type_id].revision, self.revision, "Unexpected revision")
asserts.assert_equal(len(device_type[self.device_type_id].server_clusters), len(clusters), "Unexpected number of clusters")

def test_bad_device_type_id(self):
xml = self.template.render(device_type_id="", revision=self.revision, classification_class=self.classification_class,
classification_scope=self.classification_scope, clusters=self.clusters)
et = ElementTree.fromstring(xml)
device_type, problems = parse_single_device_type(et)
asserts.assert_equal(len(problems), 1, "Device with blank ID did not generate a problem notice")

def test_bad_class(self):
xml = self.template.render(device_type_id=self.device_type_id, revision=self.revision, classification_class="",
classification_scope=self.classification_scope, clusters=self.clusters)
et = ElementTree.fromstring(xml)
device_type, problems = parse_single_device_type(et)
asserts.assert_equal(len(problems), 1, "Device with no class did not generate a problem notice")

def test_bad_scope(self):
xml = self.template.render(device_type_id=self.device_type_id, revision=self.revision, classification_class=self.classification_class,
classification_scope="", clusters=self.clusters)
et = ElementTree.fromstring(xml)
device_type, problems = parse_single_device_type(et)
asserts.assert_equal(len(problems), 1, "Device with no scope did not generate a problem notice")


if __name__ == "__main__":
default_matter_test_main()
60 changes: 29 additions & 31 deletions src/python_testing/matter_testing_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,14 +450,17 @@ class CustomCommissioningParameters:


@dataclass
class ProblemLocation:
class ClusterPathLocation:
endpoint_id: int
cluster_id: int

def __str__(self):
return "UNKNOWN"
return (f'\n Endpoint: {self.endpoint_id},'
f'\n Cluster: {cluster_id_str(self.cluster_id)}')


@dataclass
class AttributePathLocation(ProblemLocation):
endpoint_id: int
class AttributePathLocation(ClusterPathLocation):
cluster_id: Optional[int] = None
attribute_id: Optional[int] = None

Expand All @@ -475,55 +478,50 @@ def as_string(self, mapper: ClusterMapper):
return desc

def __str__(self):
return (f'\n Endpoint: {self.endpoint_id},'
f'\n Cluster: {cluster_id_str(self.cluster_id)},'
f'\n Attribute:{id_str(self.attribute_id)}')
return (f'{super().__str__()}'
f'\n Attribute:{id_str(self.attribute_id)}')


@dataclass
class EventPathLocation(ProblemLocation):
endpoint_id: int
cluster_id: int
class EventPathLocation(ClusterPathLocation):
event_id: int

def __str__(self):
return (f'\n Endpoint: {self.endpoint_id},'
f'\n Cluster: {cluster_id_str(self.cluster_id)},'
f'\n Event: {id_str(self.event_id)}')
return (f'{super().__str__()}'
f'\n Event: {id_str(self.event_id)}')


@dataclass
class CommandPathLocation(ProblemLocation):
endpoint_id: int
cluster_id: int
class CommandPathLocation(ClusterPathLocation):
command_id: int

def __str__(self):
return (f'\n Endpoint: {self.endpoint_id},'
f'\n Cluster: {cluster_id_str(self.cluster_id)},'
f'\n Command: {id_str(self.command_id)}')
return (f'{super().__str__()}'
f'\n Command: {id_str(self.command_id)}')


@dataclass
class ClusterPathLocation(ProblemLocation):
endpoint_id: int
cluster_id: int
class FeaturePathLocation(ClusterPathLocation):
feature_code: str

def __str__(self):
return (f'\n Endpoint: {self.endpoint_id},'
f'\n Cluster: {cluster_id_str(self.cluster_id)}')
return (f'{super().__str__()}'
f'\n Feature: {self.feature_code}')


@dataclass
class FeaturePathLocation(ProblemLocation):
endpoint_id: int
cluster_id: int
feature_code: str
class DeviceTypePathLocation:
device_type_id: int
cluster_id: Optional[int] = None

def __str__(self):
return (f'\n Endpoint: {self.endpoint_id},'
f'\n Cluster: {cluster_id_str(self.cluster_id)},'
f'\n Feature: {self.feature_code}')
msg = f'\n DeviceType: {self.device_type_id}'
if self.cluster_id:
msg += f'\n ClusterID: {self.cluster_id}'
return msg


ProblemLocation = typing.Union[ClusterPathLocation, DeviceTypePathLocation]

# ProblemSeverity is not using StrEnum, but rather Enum, since StrEnum only
# appeared in 3.11. To make it JSON serializable easily, multiple inheritance
Expand Down
Loading

0 comments on commit ded3be7

Please sign in to comment.