Skip to content

Commit d65edf1

Browse files
committed
TC-CCTRL-3.1: Mock'd version
1 parent 44c725d commit d65edf1

File tree

4 files changed

+248
-6
lines changed

4 files changed

+248
-6
lines changed

src/controller/python/chip/clusters/__init__.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from .Objects import (AccessControl, AccountLogin, Actions, ActivatedCarbonFilterMonitoring, AdministratorCommissioning, AirQuality,
2727
ApplicationBasic, ApplicationLauncher, AudioOutput, BallastConfiguration, BarrierControl, BasicInformation,
2828
BinaryInputBasic, Binding, BooleanState, BooleanStateConfiguration, BridgedDeviceBasicInformation,
29-
CarbonDioxideConcentrationMeasurement, CarbonMonoxideConcentrationMeasurement, Channel, ColorControl,
29+
CarbonDioxideConcentrationMeasurement, CarbonMonoxideConcentrationMeasurement, Channel, CommissionerControl, ColorControl,
3030
ContentControl, ContentLauncher, DemandResponseLoadControl, Descriptor, DeviceEnergyManagement,
3131
DeviceEnergyManagementMode, DiagnosticLogs, DishwasherAlarm, DishwasherMode, DoorLock, EcosystemInformation,
3232
ElectricalEnergyMeasurement, ElectricalMeasurement, ElectricalPowerMeasurement, EnergyEvse, EnergyEvseMode,
@@ -52,8 +52,8 @@
5252
__all__ = [Attribute, CHIPClusters, Command, AccessControl, AccountLogin, Actions, ActivatedCarbonFilterMonitoring, AdministratorCommissioning, AirQuality,
5353
ApplicationBasic, ApplicationLauncher, AudioOutput, BallastConfiguration, BarrierControl, BasicInformation,
5454
BinaryInputBasic, Binding, BooleanState, BooleanStateConfiguration, BridgedDeviceBasicInformation, CarbonDioxideConcentrationMeasurement,
55-
CarbonMonoxideConcentrationMeasurement, Channel,
56-
ColorControl, ContentControl, ContentLauncher, DemandResponseLoadControl, Descriptor, DeviceEnergyManagementMode, DeviceEnergyManagement, DeviceEnergyManagementMode, DiagnosticLogs, DishwasherAlarm, DishwasherMode,
55+
CarbonMonoxideConcentrationMeasurement, Channel, ColorControl, CommissionerControl,
56+
ContentControl, ContentLauncher, DemandResponseLoadControl, Descriptor, DeviceEnergyManagementMode, DeviceEnergyManagement, DeviceEnergyManagementMode, DiagnosticLogs, DishwasherAlarm, DishwasherMode,
5757
DoorLock, EcosystemInformation, ElectricalEnergyMeasurement, ElectricalMeasurement, ElectricalPowerMeasurement, EnergyEvse, EnergyEvseMode, EnergyPreference,
5858
EthernetNetworkDiagnostics, FanControl, FaultInjection, FixedLabel, FlowMeasurement,
5959
FormaldehydeConcentrationMeasurement, GeneralCommissioning, GeneralDiagnostics, GroupKeyManagement, Groups,

src/python_testing/TC_CCTRL.py

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#
2+
# Copyright (c) 2024 Project CHIP Authors
3+
# All rights reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments
19+
# for details about the block below.
20+
#
21+
# TODO: Skip CI for now, we don't have any way to run this. Needs setup. See test_TC_CCTRL.py
22+
23+
import ipaddress
24+
import logging
25+
import os
26+
import signal
27+
import subprocess
28+
import uuid
29+
import random
30+
import pathlib
31+
import time
32+
33+
import chip.clusters as Clusters
34+
import chip.exceptions
35+
from matter_testing_support import MatterBaseTest, default_matter_test_main, per_endpoint_test, has_cluster, async_test_body
36+
from mobly import asserts
37+
from chip.interaction_model import InteractionModelError, Status
38+
39+
# isort: off
40+
41+
from chip import ChipDeviceCtrl # Needed before chip.FabricAdmin
42+
import chip.CertificateAuthority
43+
from chip.ChipDeviceCtrl import CommissioningParameters
44+
45+
# isort: on
46+
47+
48+
class TC_CCTRL(MatterBaseTest):
49+
50+
@async_test_body
51+
async def setup_class(self):
52+
super().setup_class()
53+
# TODO: This needs to come from an arg and needs to be something available on the TH
54+
# TODO: confirm whether we can open processes like this on the TH
55+
app = os.path.join(pathlib.Path(__file__).resolve().parent, '..','..','out', 'linux-x64-all-clusters-no-ble', 'chip-all-clusters-app')
56+
57+
self.kvs = f'kvs_{str(uuid.uuid4())}'
58+
self.port = 5543
59+
discriminator = random.randint(0, 4095)
60+
discriminator = 3840
61+
passcode = 20202021
62+
app_args = f'--secured-device-port {self.port} --discriminator {discriminator} --passcode {passcode} --KVS {self.kvs}'
63+
cmd = f'{app} {app_args}'
64+
# TODO: Determine if we want these logs cooked or pushed to somewhere else
65+
logging.info("Starting TH_SERVER")
66+
self.app_process = subprocess.Popen(cmd, bufsize=0, shell=True)
67+
logging.info("TH_SERVER started")
68+
time.sleep(3)
69+
70+
logging.info("Commissioning from separate fabric")
71+
72+
73+
# Create a second controller on a new fabric to communicate to the server
74+
new_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority()
75+
new_fabric_admin = new_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=2)
76+
paa_path = str(self.matter_test_config.paa_trust_store_path)
77+
print(f"paa_path = {paa_path} ------------------------------------------------")
78+
self.TH_server_controller = new_fabric_admin.NewController(nodeId=112233, paaTrustStorePath=paa_path)
79+
self.server_nodeid = 1111
80+
await self.TH_server_controller.CommissionOnNetwork(nodeId=self.server_nodeid, setupPinCode=passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=discriminator)
81+
logging.info("Commissioning TH_SERVER complete")
82+
83+
def teardown_class(self):
84+
logging.warning("Stopping app with SIGTERM")
85+
self.app_process.send_signal(signal.SIGTERM.value)
86+
test_app_exit_code = self.app_process.wait()
87+
# TODO: Use timeout, if term doesn't work, try SIGINT
88+
89+
os.remove(self.kvs)
90+
super().teardown_class()
91+
92+
93+
#@per_endpoint_test(has_cluster(Clusters.CommissionerControl))
94+
@async_test_body
95+
async def test_TC_CCTRL_3_1(self):
96+
th_server_fabrics = await self.read_single_attribute_check_success(cluster=Clusters.OperationalCredentials, attribute=Clusters.OperationalCredentials.Attributes.Fabrics, dev_ctrl=self.TH_server_controller, node_id=self.server_nodeid, endpoint=0)
97+
th_server_vid = await self.read_single_attribute_check_success(cluster=Clusters.BasicInformation, attribute=Clusters.BasicInformation.Attributes.VendorID, dev_ctrl=self.TH_server_controller, node_id=self.server_nodeid, endpoint=0)
98+
th_server_pid = await self.read_single_attribute_check_success(cluster=Clusters.BasicInformation, attribute=Clusters.BasicInformation.Attributes.ProductID, dev_ctrl=self.TH_server_controller, node_id=self.server_nodeid, endpoint=0)
99+
100+
# TODO: Read event, not yet implemented in mock
101+
102+
ipaddr = ipaddress.IPv6Address('::1')
103+
cmd = Clusters.CommissionerControl.Commands.CommissionNode(requestId=1, responseTimeoutSeconds=30, ipAddress=ipaddr.packed, port=self.port)
104+
try:
105+
await self.send_single_cmd(cmd)
106+
asserts.fail("Unexpected success on CommissionNode")
107+
except InteractionModelError as e:
108+
asserts.assert_equal(e.status, Status.Failure, "Incorrect error returned")
109+
110+
params = await self.openCommissioningWindow(dev_ctrl=self.default_controller, node_id=self.dut_node_id)
111+
112+
pase_nodeid = self.dut_node_id + 1
113+
await self.default_controller.FindOrEstablishPASESession(setupCode=params.commissioningParameters.setupQRCode, nodeid=pase_nodeid)
114+
try:
115+
await self.send_single_cmd(cmd=cmd, node_id=pase_nodeid)
116+
asserts.fail("Unexpected success on CommissionNode")
117+
except InteractionModelError as e:
118+
asserts.assert_equal(e.status, Status.UnsupportedAccess, "Incorrect error returned")
119+
120+
good_request_id = 0x1234567887654321
121+
cmd = Clusters.CommissionerControl.Commands.RequestCommissioningApproval(requestId=good_request_id, vendorId=th_server_vid, productId=th_server_pid)
122+
try:
123+
await self.send_single_cmd(cmd=cmd, node_id=pase_nodeid)
124+
asserts.fail("Unexpected success on CommissionNode")
125+
except InteractionModelError as e:
126+
asserts.assert_equal(e.status, Status.UnsupportedAccess, "Incorrect error returned")
127+
128+
129+
# TODO: read event - need to implement in the mock
130+
131+
# If no exception is raised, this is success
132+
await self.send_single_cmd(cmd)
133+
134+
if __name__ == "__main__":
135+
default_matter_test_main()

src/python_testing/test_testing/MockTestRunner.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@
3434
class AsyncMock(MagicMock):
3535
async def __call__(self, *args, **kwargs):
3636
return super(AsyncMock, self).__call__(*args, **kwargs)
37-
38-
3937
class MockTestRunner():
4038

41-
def __init__(self, filename: str, classname: str, test: str, endpoint: int = 0, pics: dict[str, bool] = None):
39+
def __init__(self, filename: str, classname: str, test: str, endpoint: int = 0, pics: dict[str, bool] = None, paa_trust_store_path = None):
4240
self.test = test
4341
self.endpoint = endpoint
4442
self.pics = pics
43+
self.kvs_storage = 'kvs_admin.json'
44+
self.paa_path = paa_trust_store_path
4545
self.set_test(filename, classname, test)
4646
self.stack = MatterStackState(self.config)
4747
self.default_controller = self.stack.certificate_authorities[0].adminList[0].NewController(
@@ -60,6 +60,8 @@ def set_test_config(self, test_config: MatterTestConfig = MatterTestConfig()):
6060
self.config = test_config
6161
self.config.tests = [self.test]
6262
self.config.endpoint = self.endpoint
63+
self.config.storage_path = self.kvs_storage
64+
self.config.paa_trust_store_path = self.paa_path
6365
if not self.config.dut_node_ids:
6466
self.config.dut_node_ids = [1]
6567
if self.pics:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env -S python3 -B
2+
#
3+
# Copyright (c) 2024 Project CHIP Authors
4+
# All rights reserved.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
import os
20+
import sys
21+
import pathlib
22+
import typing
23+
24+
import chip.clusters as Clusters
25+
from chip import ChipDeviceCtrl
26+
from chip.clusters import Attribute
27+
from MockTestRunner import MockTestRunner, AsyncMock
28+
from chip.interaction_model import InteractionModelError, Status
29+
30+
31+
try:
32+
from matter_testing_support import get_default_paa_trust_store, run_tests_no_exit
33+
except ImportError:
34+
sys.path.append(os.path.abspath(
35+
os.path.join(os.path.dirname(__file__), '..')))
36+
from matter_testing_support import get_default_paa_trust_store, run_tests_no_exit
37+
38+
call_count = 0
39+
40+
def dynamic_return(*args, **argv):
41+
print("using Mock invoke")
42+
global call_count
43+
call_count += 1
44+
45+
if call_count == 1: # Commission node with no prior request, return failure
46+
raise InteractionModelError(status=Status.Failure)
47+
elif call_count == 2: # Commission node over pase - return unsupported access
48+
raise InteractionModelError(status=Status.UnsupportedAccess)
49+
elif call_count == 3: # request commissioning approval over pase - return unsupported access
50+
raise InteractionModelError(status=Status.UnsupportedAccess)
51+
elif call_count == 4: # good RequestCommissioningApproval over CASE
52+
return None
53+
else:
54+
raise InteractionModelError(Status.Failure)
55+
56+
def wildcard() -> Attribute.AsyncReadTransaction.ReadResponse:
57+
cc = Clusters.CommissionerControl
58+
ei = Clusters.EcosystemInformation
59+
desc = Clusters.Descriptor
60+
bdbi = Clusters.BridgedDeviceBasicInformation
61+
62+
# EP1 is aggregator device type with a commissioner control cluster
63+
# children - EP2 type bridged node endpoint, ecosystem information, bridged device basic information. Should also have and admin commissioning, but I don't need it for this test.
64+
desc_ep1 = {desc.Attributes.PartsList: [2], desc.Attributes.ServerList: [cc.id], desc.Attributes.DeviceTypeList: [desc.Structs.DeviceTypeStruct(deviceType=0x000E, revision=2)]}
65+
desc_ep2 = {desc.Attributes.ServerList: [bdbi.id, ei.id], desc.Attributes.DeviceTypeList: [desc.Structs.DeviceTypeStruct(deviceType=0x0013, revision=3)]}
66+
67+
# I'm not filling anything in here, because I don't care. I just care that the cluster exists.
68+
ei_attrs = {ei.Attributes.AttributeList:[ei.Attributes.DeviceDirectory.attribute_id, ei.Attributes.LocationDirectory.attribute_id], ei.Attributes.DeviceDirectory:[], ei.Attributes.LocationDirectory:[]}
69+
70+
# This cluster just needs to exist, so I'm just going to throw on the mandatory items for now.
71+
bdbi_attrs = {bdbi.Attributes.AttributeList:[bdbi.Attributes.Reachable.attribute_id, bdbi.Attributes.UniqueID.attribute_id], bdbi.Attributes.Reachable:True, bdbi.Attributes.UniqueID:'something'}
72+
73+
cc_attrs = {cc.Attributes.AttributeList:[cc.Attributes.SupportedDeviceCategories], cc.Attributes.AcceptedCommandList:[cc.Commands.RequestCommissioningApproval, cc.Commands.CommissionNode],
74+
cc.Attributes.GeneratedCommandList:[cc.Commands.RequestCommissioningApproval], cc.Attributes.SupportedDeviceCategories:1}
75+
76+
resp = Attribute.AsyncReadTransaction.ReadResponse({}, [], {})
77+
resp.attributes = {1: {desc: desc_ep1, cc:cc_attrs}, 2:{desc:desc_ep2, ei:ei_attrs, bdbi:bdbi_attrs}}
78+
return resp
79+
80+
class MyMock(MockTestRunner):
81+
# TODO consolidate with above
82+
def run_test_with_mock(self, dynamic_invoke_return: typing.Callable, read_cache: Attribute.AsyncReadTransaction.ReadResponse, hooks=None):
83+
''' Effects is a list of callable functions with *args, **kwargs parameters. It can either throw an InteractionModelException or return the command response.'''
84+
self.default_controller.Read = AsyncMock(return_value=read_cache)
85+
self.default_controller.SendCommand = AsyncMock(return_value=None, side_effect=dynamic_invoke_return)
86+
# It doesn't actually matter what we return here because I'm going to catch the next pase session connection anyway
87+
params = ChipDeviceCtrl.CommissioningParameters(setupPinCode=0, setupManualCode='', setupQRCode='')
88+
self.default_controller.OpenCommissioningWindow = AsyncMock(return_value=params)
89+
self.default_controller.FindOrEstablishPASESession = AsyncMock(return_value=None)
90+
91+
return run_tests_no_exit(self.test_class, self.config, hooks, self.default_controller, self.stack)
92+
93+
def main():
94+
root = os.path.abspath(os.path.join(pathlib.Path(__file__).resolve().parent, '..','..','..'))
95+
print(f'root = {root}')
96+
paa_path = get_default_paa_trust_store(root)
97+
print(f'paa = {paa_path}')
98+
99+
test_runner = MyMock('TC_CCTRL', 'TC_CCTRL', 'test_TC_CCTRL_3_1', 1, paa_trust_store_path=paa_path)
100+
101+
test_runner.run_test_with_mock(dynamic_return, wildcard())
102+
test_runner.Shutdown()
103+
104+
if __name__ == "__main__":
105+
sys.exit(main())

0 commit comments

Comments
 (0)