Skip to content

Commit 480a0dd

Browse files
Software Update base class (project-chip#41145)
* SUBase Test class start * OTA Provider Wrapper changes for SU 2.7 * Fix Wrapper updated * Added kwargs as the params as we are just forwarding params * Updates for OTAProviderSubprocess * Treat the logs as separate. * Changes for SoftwareUpdate base class * Removed static values. Updated methods with defaults. Added commissionprovider method. * Mypy fixes * Fix the log_path in base class. Added some method description * Updated name for TC_SUTestBase.py added TC_SUTestBase.py into not_automated at test_metadata.yaml * Close fd if is not None * Ignore TC_SUTestBase.py * Methods updated * Removed hardcoded from launch_provider, accept kwargs and filter arguments. Added method to get ota_image_path * Fix ruff check * Update wrapper: removed acl. Updated SUBaseTest added create_acl_entry and removed commisionprovider from base. Added Class ACL Handler into SuBaseTest as it will be used by in other tests. * Removed unused import * Update TC_SUTestBase.py Fixed method name typo * Restyled by autopep8 * Added Requestor to wrapper * Added changes for RequestorSubProcess * Update TC_SUTestBase.py Fix: invalid method * Removed requestor wrapper. Removed requestor start. Removed hardcoded paths. ota-image path and chip-ota-provider path will be provider to the test as an argument, Removed static versions version should be provided in the test as argument. * Mypy fixes. Review fixes. * Update append and read logs * Fix typo in variable * ANSI clean chagnes * Dont delete the file * Remove read_from_logs * Remove ACLHandler Class and remove unused import --------- Co-authored-by: Restyled.io <[email protected]>
1 parent d5df4bc commit 480a0dd

File tree

4 files changed

+362
-73
lines changed

4 files changed

+362
-73
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
#
2+
# Copyright (c) 2025 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+
import logging
19+
import tempfile
20+
from os import path
21+
from typing import Optional
22+
23+
from mobly import asserts
24+
25+
import matter.clusters as Clusters
26+
from matter import ChipDeviceCtrl
27+
from matter.clusters.Types import NullValue
28+
from matter.interaction_model import Status
29+
from matter.testing.apps import OtaImagePath, OTAProviderSubprocess
30+
from matter.testing.matter_testing import MatterBaseTest
31+
32+
logger = logging.getLogger(__name__)
33+
34+
35+
class SoftwareUpdateBaseTest(MatterBaseTest):
36+
"""This is the base test class for SoftwareUpdate Test Cases"""
37+
38+
current_provider_app_proc: Optional[OTAProviderSubprocess] = None
39+
provider_app_path: Optional[str] = None
40+
41+
def start_provider(self,
42+
provider_app_path: str = "",
43+
ota_image_path: str = "",
44+
setup_pincode: int = 20202021,
45+
discriminator: int = 1234,
46+
port: int = 5541,
47+
storage_dir='/tmp',
48+
extra_args: list = [],
49+
kvs_path: Optional[str] = None,
50+
log_file: Optional[str] = None, expected_output: str = "Status: Satisfied",
51+
timeout: int = 10):
52+
"""Start the provider process using the provided configuration.
53+
54+
Args:
55+
provider_app_path (str): Path of Requestor app to load.
56+
ota_image_path (str): Ota image to load within the provider.
57+
setup_pincode (int, optional): Setup pincode for the provider process. Defaults to 20202021.
58+
discriminator (int, optional): Discriminator for the provider process. Defaults to 1234.
59+
port (int, optional): Port for the provider process. Defaults to 5541.
60+
storage_dir (str, optional): Storage dir for the provider proccess. Defaults to '/tmp'.
61+
extra_args (list, optional): Extra args to send to the provider process. Defaults to [].
62+
kvs_path(str): Str of the path for the kvs path, if not will use temp file.
63+
log_file (Optional[str], optional): Destination for the app process logs. Defaults to None.
64+
expected_output (str): Expected string to see after a default timeout. Defaults to "Status: Satisfied".
65+
timeout (int): Timeout to wait for the expected output. Defaults to 10 seconds
66+
"""
67+
logger.info(f"Launching provider app with with ota image {ota_image_path}")
68+
# Image to launch
69+
self.provider_app_path = provider_app_path
70+
if not path.exists(provider_app_path):
71+
raise FileNotFoundError(f"Provider app not found {provider_app_path}")
72+
73+
if not path.exists(ota_image_path):
74+
raise FileNotFoundError(f"Ota image provided does not exists {ota_image_path}")
75+
76+
# Ota image
77+
ota_image_path = OtaImagePath(path=ota_image_path)
78+
# Ideally we send the logs to a fixed location to avoid conflicts
79+
80+
if log_file is None:
81+
# Assign the file descriptor to log_file
82+
log_file = tempfile.NamedTemporaryFile(
83+
dir=storage_dir, prefix='provider_', suffix='.log', mode='ab')
84+
logger.info(f"Writing Provider logs at :{log_file.name}")
85+
else:
86+
logger.info(f"Writing Provider logs at : {log_file}")
87+
# Launch the Provider subprocess using the Wrapper
88+
proc = OTAProviderSubprocess(
89+
provider_app_path,
90+
storage_dir=storage_dir,
91+
port=port,
92+
discriminator=discriminator,
93+
passcode=setup_pincode,
94+
ota_source=ota_image_path,
95+
extra_args=extra_args,
96+
kvs_path=kvs_path,
97+
log_file=log_file,
98+
err_log_file=log_file)
99+
proc.start(
100+
expected_output=expected_output,
101+
timeout=timeout)
102+
103+
self.current_provider_app_proc = proc
104+
logger.info(f"Provider started with PID: {self.current_provider_app_proc.get_pid()}")
105+
106+
async def announce_ota_provider(self,
107+
controller: ChipDeviceCtrl,
108+
provider_node_id: int,
109+
requestor_node_id: int,
110+
reason: Clusters.OtaSoftwareUpdateRequestor.Enums.AnnouncementReasonEnum = Clusters.OtaSoftwareUpdateRequestor.Enums.AnnouncementReasonEnum.kUpdateAvailable,
111+
vendor_id: int = 0xFFF1,
112+
endpoint: int = 0):
113+
""" Launch the requestor.AnnounceOTAProvider method with the specific configuration.
114+
Starts the communication from the requestor to the provider to start a software update.
115+
Args:
116+
controller (ChipDeviceCtrl): Controller for DUT
117+
provider_node_id (int): Node id for the provider
118+
requestor_node_id (int): Node id for the requestor
119+
reason (Clusters.OtaSoftwareUpdateRequestor.Enums.AnnouncementReasonEnum, optional): Update Reason. Defaults to Clusters.OtaSoftwareUpdateRequestor.Enums.AnnouncementReasonEnum.kUpdateAvailable.
120+
vendor_id (int, optional): Vendor id. Defaults to 0xFFF1.
121+
endpoint (int, optional): Endpoint id. Defaults to 0.
122+
123+
Returns:
124+
object: Return the data from the OtaSoftwareUpdateRequestor.AnnounceOTAProvider command.
125+
"""
126+
cmd_announce_ota_provider = Clusters.OtaSoftwareUpdateRequestor.Commands.AnnounceOTAProvider(
127+
providerNodeID=provider_node_id,
128+
vendorID=vendor_id,
129+
announcementReason=reason,
130+
metadataForNode=None,
131+
endpoint=endpoint
132+
)
133+
logger.info("Sending AnnounceOTA Provider Command")
134+
cmd_resp = await self.send_single_cmd(
135+
cmd=cmd_announce_ota_provider,
136+
dev_ctrl=controller,
137+
node_id=requestor_node_id,
138+
endpoint=endpoint,
139+
)
140+
logger.info(f"Announce command sent {cmd_resp}")
141+
return cmd_resp
142+
143+
async def set_default_ota_providers_list(self, controller: ChipDeviceCtrl, provider_node_id: int, requestor_node_id: int, endpoint: int = 0):
144+
"""Write the provider list in the requestor to initiate the Software Update.
145+
146+
Args:
147+
controller (ChipDeviceCtrl): Controller to write the providers.
148+
provider_node_id (int): Node where the provider is localted.
149+
requestor_node_id (int): Node of the requestor to write the providers.
150+
endpoint (int, optional): Endpoint to write the providerss. Defaults to 0.
151+
"""
152+
153+
current_otap_info = await self.read_single_attribute_check_success(
154+
dev_ctrl=controller,
155+
cluster=Clusters.OtaSoftwareUpdateRequestor,
156+
attribute=Clusters.OtaSoftwareUpdateRequestor.Attributes.DefaultOTAProviders
157+
)
158+
logger.info(f"OTA Providers: {current_otap_info}")
159+
160+
# Create Provider Location into Requestor
161+
provider_location_struct = Clusters.OtaSoftwareUpdateRequestor.Structs.ProviderLocation(
162+
providerNodeID=provider_node_id,
163+
endpoint=endpoint,
164+
fabricIndex=controller.fabricId
165+
)
166+
167+
# Create the OTA Provider Attribute
168+
ota_providers_attr = Clusters.OtaSoftwareUpdateRequestor.Attributes.DefaultOTAProviders(value=[provider_location_struct])
169+
170+
# Write the Attribute
171+
resp = await controller.WriteAttribute(
172+
attributes=[(endpoint, ota_providers_attr)],
173+
nodeid=requestor_node_id,
174+
)
175+
asserts.assert_equal(resp[0].Status, Status.Success, "Failed to write Default OTA Providers Attribute")
176+
177+
# Read Updated OTAProviders
178+
after_otap_info = await self.read_single_attribute_check_success(
179+
dev_ctrl=controller,
180+
cluster=Clusters.OtaSoftwareUpdateRequestor,
181+
attribute=Clusters.OtaSoftwareUpdateRequestor.Attributes.DefaultOTAProviders
182+
)
183+
logger.info(f"OTA Providers List: {after_otap_info}")
184+
185+
async def verify_version_applied_basic_information(self, controller: ChipDeviceCtrl, node_id: int, target_version: int):
186+
"""Verify the version from the BasicInformationCluster and compares against the provider target version.
187+
188+
Args:
189+
controller (ChipDeviceCtrl): Controller
190+
node_id (int): Node to request
191+
target_version (int): Version to compare
192+
"""
193+
194+
basicinfo_softwareversion = await self.read_single_attribute_check_success(
195+
dev_ctrl=controller,
196+
cluster=Clusters.BasicInformation,
197+
attribute=Clusters.BasicInformation.Attributes.SoftwareVersion,
198+
node_id=node_id)
199+
asserts.assert_equal(basicinfo_softwareversion, target_version,
200+
f"Version from basic info cluster is not {target_version}, current cluster version is {basicinfo_softwareversion}")
201+
202+
def get_downloaded_ota_image_info(self, ota_path='/tmp/test.bin') -> dict:
203+
"""Return the data of the downloaded image from the provider.
204+
205+
Args:
206+
ota_path (str, optional): _description_. Defaults to '/tmp/test.bin'.
207+
208+
Returns:
209+
dict: Dict with the image info.
210+
"""
211+
ota_image_info = {
212+
"path": ota_path,
213+
"exists": False,
214+
"size": 0,
215+
}
216+
try:
217+
ota_image_info['size'] = path.getsize(ota_path)
218+
ota_image_info['exists'] = True
219+
except OSError:
220+
logger.info(f"OTA IMAGE at {ota_path} does not exists")
221+
return ota_image_info
222+
223+
return ota_image_info
224+
225+
def verify_state_transition_event(self,
226+
event_report: Clusters.OtaSoftwareUpdateRequestor.Events.StateTransition,
227+
expected_previous_state,
228+
expected_new_state,
229+
expected_target_version: Optional[int] = None,
230+
expected_reason: Optional[int] = None):
231+
"""Verify the values of the StateTransitionEvent from the EventHandler given the provided arguments.
232+
233+
Args:
234+
event_report (Clusters.OtaSoftwareUpdateRequestor.Events.StateTransition): StateTransition Event report to verify.
235+
previous_state (UpdateStateEnum:int): Int or UpdateStateEnum value for the previous state.
236+
new_state (UpdateStateEnum:int): Int or UpdateStateEnum value for the new or current state.
237+
target_version (Optional[int], optional): Software version to verify if not provided ignore this check.. Defaults to None.
238+
reason (Optional[int], optional): UpdateStateEnum reason of the event, if not provided ignore. Defaults to None.
239+
"""
240+
logger.info(f"Verifying the event {event_report}")
241+
asserts.assert_equal(event_report.previousState, expected_previous_state,
242+
f"Previous state was not {expected_previous_state}")
243+
asserts.assert_equal(event_report.newState, expected_new_state, f"New state is not {expected_new_state}")
244+
if expected_target_version is not None:
245+
asserts.assert_equal(event_report.targetSoftwareVersion, expected_target_version,
246+
f"Target version is not {expected_target_version}")
247+
if expected_reason is not None:
248+
asserts.assert_equal(event_report.reason, expected_reason, f"Reason is not {expected_reason}")
249+
250+
def create_acl_entry(self,
251+
dev_ctrl: ChipDeviceCtrl.ChipDeviceController,
252+
provider_node_id: int,
253+
requestor_node_id: Optional[int] = None,
254+
acl_entries: Optional[list[Clusters.AccessControl.Structs.AccessControlEntryStruct]] = None,
255+
):
256+
"""Create ACL entries to allow OTA requestors to access the provider.
257+
258+
Args:
259+
dev_ctrl: Device controller for sending commands
260+
provider_node_id: Node ID of the OTA provider
261+
requestor_node_id: Optional specific requestor node ID for targeted access
262+
acl_entries: Optional[list[Clusters.AccessControl.Structs.AccessControlEntryStruct]]. ACL list to write ino the requestor.
263+
264+
Returns:
265+
Result of the ACL write operation
266+
"""
267+
# Standard ACL entry for OTA Provider cluster
268+
admin_node_id = dev_ctrl.nodeId if hasattr(dev_ctrl, 'nodeId') else self.DEFAULT_ADMIN_NODE_ID
269+
requestor_subjects = [requestor_node_id] if requestor_node_id else NullValue
270+
271+
if acl_entries is None:
272+
# If there are not ACL entries using proper struct constructors create the default.
273+
acl_entries = [
274+
# Admin entry
275+
Clusters.AccessControl.Structs.AccessControlEntryStruct( # type: ignore
276+
privilege=Clusters.AccessControl.Enums.AccessControlEntryPrivilegeEnum.kAdminister, # type: ignore
277+
authMode=Clusters.AccessControl.Enums.AccessControlEntryAuthModeEnum.kCase, # type: ignore
278+
subjects=[admin_node_id], # type: ignore
279+
targets=NullValue
280+
),
281+
# Operate entry
282+
Clusters.AccessControl.Structs.AccessControlEntryStruct( # type: ignore
283+
privilege=Clusters.AccessControl.Enums.AccessControlEntryPrivilegeEnum.kOperate, # type: ignore
284+
authMode=Clusters.AccessControl.Enums.AccessControlEntryAuthModeEnum.kCase, # type: ignore
285+
subjects=requestor_subjects, # type: ignore
286+
targets=[
287+
Clusters.AccessControl.Structs.AccessControlTargetStruct( # type: ignore
288+
cluster=Clusters.OtaSoftwareUpdateProvider.id, # type: ignore
289+
endpoint=NullValue,
290+
deviceType=NullValue
291+
)
292+
],
293+
)
294+
]
295+
296+
# Create the attribute descriptor for the ACL attribute
297+
acl_attribute = Clusters.AccessControl.Attributes.Acl(acl_entries)
298+
299+
return dev_ctrl.WriteAttribute(
300+
nodeid=provider_node_id,
301+
attributes=[(0, acl_attribute)]
302+
)

0 commit comments

Comments
 (0)