Skip to content

Commit b115216

Browse files
agnersaustina-csa
authored andcommitted
[Python] Convert async API functions to python asyncio (project-chip#33989)
* [Python] Use context manager for Commissioning Use a context manager to handle the commissioning process in the device controller. This will ensure that the commissioning resources are properly cleaned up after completion and removes boiler plate code. Also clear fabricCheckNodeId and mark it internal use by adding the underline prefix. Also call pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete directly on the Python Thread, as this is an atomic operation. This is will also be more asyncio friendly as it is guaranteed to not block. * [Python] Use context manager for all callbacks Use context managers for all APIs which wait for callbacks. This allows to cleanly wrap the future and add additional handling e.g. locks for asyncio in the future. * [Python] Convert commissioning APIs to async functions Make all commissioning APIs async functions. This avoids the need to use run_in_executor() to call them from asyncio code in a non- blocking way. * [Python] Convert UnpairDevice/OpenCommissioningWindow to asyncio * [Python] Convert EstablishPASESession to asyncio * [Python] Convert IssueNOCChain to asyncio * [Python] Add locking to prevent concurrent access with asyncio Make sure that different asyncio tasks do not run the same function concurrently. This is done by adding an asyncio lock to functions which use callbacks. * [Python] Raise an exception if the future did not complete * [Python] Convert tests in src/controller/python/ to asyncio * [Python] Convert tests in src/python_testing/ to asyncio * Adjust yamltest_with_chip_repl_tester to use asyncio * [Python] Add documentation to the new context managers * [Python] Use asyncio.run() to run async tests
1 parent 1cb142e commit b115216

28 files changed

+331
-322
lines changed

scripts/tests/chiptest/yamltest_with_chip_repl_tester.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ async def execute_test(yaml, runner):
101101
'--pics-file',
102102
default=None,
103103
help='Optional PICS file')
104-
def main(setup_code, yaml_path, node_id, pics_file):
104+
async def main(setup_code, yaml_path, node_id, pics_file):
105105
# Setting up python environment for running YAML CI tests using python parser.
106106
with tempfile.NamedTemporaryFile() as chip_stack_storage:
107107
chip.native.Init()
@@ -122,7 +122,7 @@ def main(setup_code, yaml_path, node_id, pics_file):
122122
# Creating and commissioning to a single controller to match what is currently done when
123123
# running.
124124
dev_ctrl = ca_list[0].adminList[0].NewController()
125-
dev_ctrl.CommissionWithCode(setup_code, node_id)
125+
await dev_ctrl.CommissionWithCode(setup_code, node_id)
126126

127127
def _StackShutDown():
128128
# Tearing down chip stack. If not done in the correct order test will fail.
@@ -143,7 +143,7 @@ def _StackShutDown():
143143
runner = ReplTestRunner(
144144
clusters_definitions, certificate_authority_manager, dev_ctrl)
145145

146-
asyncio.run(execute_test(yaml, runner))
146+
await execute_test(yaml, runner)
147147

148148
except Exception:
149149
print(traceback.format_exc())
@@ -153,4 +153,4 @@ def _StackShutDown():
153153

154154

155155
if __name__ == '__main__':
156-
main()
156+
asyncio.run(main())

src/controller/python/chip/ChipDeviceCtrl.py

+159-149
Large diffs are not rendered by default.

src/controller/python/chip/commissioning/pase.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ def __exit__(self, type, value, traceback):
4444
self.devCtrl.CloseBLEConnection(self.is_ble)
4545

4646

47-
def establish_session(devCtrl: ChipDeviceCtrl.ChipDeviceControllerBase, parameter: commissioning.PaseParameters) -> ContextManager:
47+
async def establish_session(devCtrl: ChipDeviceCtrl.ChipDeviceControllerBase, parameter: commissioning.PaseParameters) -> ContextManager:
4848
if isinstance(parameter, commissioning.PaseOverBLEParameters):
49-
devCtrl.EstablishPASESessionBLE(parameter.setup_pin, parameter.discriminator, parameter.temporary_nodeid)
49+
await devCtrl.EstablishPASESessionBLE(parameter.setup_pin, parameter.discriminator, parameter.temporary_nodeid)
5050
elif isinstance(parameter, commissioning.PaseOverIPParameters):
5151
device = devCtrl.DiscoverCommissionableNodes(filterType=discovery.FilterType.LONG_DISCRIMINATOR,
5252
filter=parameter.long_discriminator, stopOnFirst=True)
@@ -63,7 +63,7 @@ def establish_session(devCtrl: ChipDeviceCtrl.ChipDeviceControllerBase, paramete
6363
break
6464
if selected_address is None:
6565
raise ValueError("The node for commissioning does not contains routable ip addresses information")
66-
devCtrl.EstablishPASESessionIP(selected_address, parameter.setup_pin, parameter.temporary_nodeid)
66+
await devCtrl.EstablishPASESessionIP(selected_address, parameter.setup_pin, parameter.temporary_nodeid)
6767
else:
6868
raise TypeError("Expect PaseOverBLEParameters or PaseOverIPParameters for establishing PASE session")
6969
return ContextManager(

src/controller/python/chip/utils/CommissioningBuildingBlocks.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ async def AddNOCForNewFabricFromExisting(commissionerDevCtrl, newFabricDevCtrl,
167167

168168
csrForAddNOC = await commissionerDevCtrl.SendCommand(existingNodeId, 0, opCreds.Commands.CSRRequest(CSRNonce=os.urandom(32)))
169169

170-
chainForAddNOC = newFabricDevCtrl.IssueNOCChain(csrForAddNOC, newNodeId)
170+
chainForAddNOC = await newFabricDevCtrl.IssueNOCChain(csrForAddNOC, newNodeId)
171171
if (chainForAddNOC.rcacBytes is None or
172172
chainForAddNOC.icacBytes is None or
173173
chainForAddNOC.nocBytes is None or chainForAddNOC.ipkBytes is None):
@@ -225,7 +225,7 @@ async def UpdateNOC(devCtrl, existingNodeId, newNodeId):
225225
return False
226226
csrForUpdateNOC = await devCtrl.SendCommand(
227227
existingNodeId, 0, opCreds.Commands.CSRRequest(CSRNonce=os.urandom(32), isForUpdateNOC=True))
228-
chainForUpdateNOC = devCtrl.IssueNOCChain(csrForUpdateNOC, newNodeId)
228+
chainForUpdateNOC = await devCtrl.IssueNOCChain(csrForUpdateNOC, newNodeId)
229229
if (chainForUpdateNOC.rcacBytes is None or
230230
chainForUpdateNOC.icacBytes is None or
231231
chainForUpdateNOC.nocBytes is None or chainForUpdateNOC.ipkBytes is None):

src/controller/python/chip/yaml/runner.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ async def run_action(self, dev_ctrl: ChipDeviceController) -> _ActionResult:
665665
return _ActionResult(status=_ActionStatus.SUCCESS, response=_GetCommissionerNodeIdResult(dev_ctrl.nodeId))
666666

667667
try:
668-
dev_ctrl.CommissionWithCode(self._setup_payload, self._node_id)
668+
await dev_ctrl.CommissionWithCode(self._setup_payload, self._node_id)
669669
return _ActionResult(status=_ActionStatus.SUCCESS, response=None)
670670
except ChipStackError:
671671
return _ActionResult(status=_ActionStatus.ERROR, response=None)

src/controller/python/py_matter_yamltest_repl_adapter/matter_yamltest_repl_adapter/runner.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ async def start(self):
5959
# device with the provided node id.
6060
if self._node_id_to_commission is not None:
6161
# Magic value is the defaults expected for YAML tests.
62-
dev_ctrl.CommissionWithCode('MT:-24J0AFN00KA0648G00', self._node_id_to_commission)
62+
await dev_ctrl.CommissionWithCode('MT:-24J0AFN00KA0648G00', self._node_id_to_commission)
6363

6464
self._chip_stack = chip_stack
6565
self._certificate_authority_manager = certificate_authority_manager

src/controller/python/test/test_scripts/base.py

+24-24
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ def CreateNewFabricController(self):
234234
async def TestRevokeCommissioningWindow(self, ip: str, setuppin: int, nodeid: int):
235235
await self.devCtrl.SendCommand(
236236
nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), timedRequestTimeoutMs=10000)
237-
if not self.TestPaseOnly(ip=ip, setuppin=setuppin, nodeid=nodeid, devCtrl=self.devCtrl2):
237+
if not await self.TestPaseOnly(ip=ip, setuppin=setuppin, nodeid=nodeid, devCtrl=self.devCtrl2):
238238
return False
239239

240240
await self.devCtrl2.SendCommand(
@@ -248,17 +248,17 @@ async def TestRevokeCommissioningWindow(self, ip: str, setuppin: int, nodeid: in
248248
nodeid, 0, Clusters.AdministratorCommissioning.Commands.RevokeCommissioning(), timedRequestTimeoutMs=10000)
249249
return True
250250

251-
def TestEnhancedCommissioningWindow(self, ip: str, nodeid: int):
252-
params = self.devCtrl.OpenCommissioningWindow(nodeid=nodeid, timeout=600, iteration=10000, discriminator=3840, option=1)
253-
return self.TestPaseOnly(ip=ip, nodeid=nodeid, setuppin=params.setupPinCode, devCtrl=self.devCtrl2)
251+
async def TestEnhancedCommissioningWindow(self, ip: str, nodeid: int):
252+
params = await self.devCtrl.OpenCommissioningWindow(nodeid=nodeid, timeout=600, iteration=10000, discriminator=3840, option=1)
253+
return await self.TestPaseOnly(ip=ip, nodeid=nodeid, setuppin=params.setupPinCode, devCtrl=self.devCtrl2)
254254

255-
def TestPaseOnly(self, ip: str, setuppin: int, nodeid: int, devCtrl=None):
255+
async def TestPaseOnly(self, ip: str, setuppin: int, nodeid: int, devCtrl=None):
256256
if devCtrl is None:
257257
devCtrl = self.devCtrl
258258
self.logger.info(
259259
"Attempting to establish PASE session with device id: {} addr: {}".format(str(nodeid), ip))
260260
try:
261-
devCtrl.EstablishPASESessionIP(ip, setuppin, nodeid)
261+
await devCtrl.EstablishPASESessionIP(ip, setuppin, nodeid)
262262
except ChipStackException:
263263
self.logger.info(
264264
"Failed to establish PASE session with device id: {} addr: {}".format(str(nodeid), ip))
@@ -267,11 +267,11 @@ def TestPaseOnly(self, ip: str, setuppin: int, nodeid: int, devCtrl=None):
267267
"Successfully established PASE session with device id: {} addr: {}".format(str(nodeid), ip))
268268
return True
269269

270-
def TestCommissionOnly(self, nodeid: int):
270+
async def TestCommissionOnly(self, nodeid: int):
271271
self.logger.info(
272272
"Commissioning device with id {}".format(nodeid))
273273
try:
274-
self.devCtrl.Commission(nodeid)
274+
await self.devCtrl.Commission(nodeid)
275275
except ChipStackException:
276276
self.logger.info(
277277
"Failed to commission device with id {}".format(str(nodeid)))
@@ -280,17 +280,17 @@ def TestCommissionOnly(self, nodeid: int):
280280
"Successfully commissioned device with id {}".format(str(nodeid)))
281281
return True
282282

283-
def TestKeyExchangeBLE(self, discriminator: int, setuppin: int, nodeid: int):
283+
async def TestKeyExchangeBLE(self, discriminator: int, setuppin: int, nodeid: int):
284284
self.logger.info(
285285
"Conducting key exchange with device {}".format(discriminator))
286-
if not self.devCtrl.ConnectBLE(discriminator, setuppin, nodeid):
286+
if not await self.devCtrl.ConnectBLE(discriminator, setuppin, nodeid):
287287
self.logger.info(
288288
"Failed to finish key exchange with device {}".format(discriminator))
289289
return False
290290
self.logger.info("Device finished key exchange.")
291291
return True
292292

293-
def TestCommissionFailure(self, nodeid: int, failAfter: int):
293+
async def TestCommissionFailure(self, nodeid: int, failAfter: int):
294294
self.devCtrl.ResetTestCommissioner()
295295
a = self.devCtrl.SetTestCommissionerSimulateFailureOnStage(failAfter)
296296
if not a:
@@ -299,43 +299,43 @@ def TestCommissionFailure(self, nodeid: int, failAfter: int):
299299

300300
self.logger.info(
301301
"Commissioning device, expecting failure after stage {}".format(failAfter))
302-
self.devCtrl.Commission(nodeid)
302+
await self.devCtrl.Commission(nodeid)
303303
return self.devCtrl.CheckTestCommissionerCallbacks() and self.devCtrl.CheckTestCommissionerPaseConnection(nodeid)
304304

305-
def TestCommissionFailureOnReport(self, nodeid: int, failAfter: int):
305+
async def TestCommissionFailureOnReport(self, nodeid: int, failAfter: int):
306306
self.devCtrl.ResetTestCommissioner()
307307
a = self.devCtrl.SetTestCommissionerSimulateFailureOnReport(failAfter)
308308
if not a:
309309
# We're not going to hit this stage during commissioning so no sense trying, just say it was fine.
310310
return True
311311
self.logger.info(
312312
"Commissioning device, expecting failure on report for stage {}".format(failAfter))
313-
self.devCtrl.Commission(nodeid)
313+
await self.devCtrl.Commission(nodeid)
314314
return self.devCtrl.CheckTestCommissionerCallbacks() and self.devCtrl.CheckTestCommissionerPaseConnection(nodeid)
315315

316-
def TestCommissioning(self, ip: str, setuppin: int, nodeid: int):
316+
async def TestCommissioning(self, ip: str, setuppin: int, nodeid: int):
317317
self.logger.info("Commissioning device {}".format(ip))
318318
try:
319-
self.devCtrl.CommissionIP(ip, setuppin, nodeid)
319+
await self.devCtrl.CommissionIP(ip, setuppin, nodeid)
320320
except ChipStackException:
321321
self.logger.exception(
322322
"Failed to finish commissioning device {}".format(ip))
323323
return False
324324
self.logger.info("Commissioning finished.")
325325
return True
326326

327-
def TestCommissioningWithSetupPayload(self, setupPayload: str, nodeid: int, discoveryType: int = 2):
327+
async def TestCommissioningWithSetupPayload(self, setupPayload: str, nodeid: int, discoveryType: int = 2):
328328
self.logger.info("Commissioning device with setup payload {}".format(setupPayload))
329329
try:
330-
self.devCtrl.CommissionWithCode(setupPayload, nodeid, chip.discovery.DiscoveryType(discoveryType))
330+
await self.devCtrl.CommissionWithCode(setupPayload, nodeid, chip.discovery.DiscoveryType(discoveryType))
331331
except ChipStackException:
332332
self.logger.exception(
333333
"Failed to finish commissioning device {}".format(setupPayload))
334334
return False
335335
self.logger.info("Commissioning finished.")
336336
return True
337337

338-
def TestOnNetworkCommissioning(self, discriminator: int, setuppin: int, nodeid: int, ip_override: str = None):
338+
async def TestOnNetworkCommissioning(self, discriminator: int, setuppin: int, nodeid: int, ip_override: str = None):
339339
self.logger.info("Testing discovery")
340340
device = self.TestDiscovery(discriminator=discriminator)
341341
if not device:
@@ -345,7 +345,7 @@ def TestOnNetworkCommissioning(self, discriminator: int, setuppin: int, nodeid:
345345
if ip_override:
346346
address = ip_override
347347
self.logger.info("Testing commissioning")
348-
if not self.TestCommissioning(address, setuppin, nodeid):
348+
if not await self.TestCommissioning(address, setuppin, nodeid):
349349
self.logger.info("Failed to finish commissioning")
350350
return False
351351
return True
@@ -792,7 +792,7 @@ async def TestMultiFabric(self, ip: str, setuppin: int, nodeid: int):
792792
self.controllerNodeId, self.paaTrustStorePath)
793793

794794
try:
795-
self.devCtrl2.CommissionIP(ip, setuppin, nodeid)
795+
await self.devCtrl2.CommissionIP(ip, setuppin, nodeid)
796796
except ChipStackException:
797797
self.logger.exception(
798798
"Failed to finish key exchange with device {}".format(ip))
@@ -1313,15 +1313,15 @@ def TestNonControllerAPIs(self):
13131313
return False
13141314
return True
13151315

1316-
def TestFabricScopedCommandDuringPase(self, nodeid: int):
1316+
async def TestFabricScopedCommandDuringPase(self, nodeid: int):
13171317
'''Validates that fabric-scoped commands fail during PASE with UNSUPPORTED_ACCESS
13181318
13191319
The nodeid is the PASE pseudo-node-ID used during PASE establishment
13201320
'''
13211321
status = None
13221322
try:
1323-
asyncio.run(self.devCtrl.SendCommand(
1324-
nodeid, 0, Clusters.OperationalCredentials.Commands.UpdateFabricLabel("roboto")))
1323+
await self.devCtrl.SendCommand(
1324+
nodeid, 0, Clusters.OperationalCredentials.Commands.UpdateFabricLabel("roboto"))
13251325
except IM.InteractionModelError as ex:
13261326
status = ex.status
13271327

src/controller/python/test/test_scripts/commissioning_failure_test.py

+16-16
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
GROUP_ID = 0
4747

4848

49-
def main():
49+
async def main():
5050
optParser = OptionParser()
5151
optParser.add_option(
5252
"-t",
@@ -98,32 +98,32 @@ def main():
9898
# TODO: Start at stage 2 once handling for arming failsafe on pase is done.
9999
if options.report:
100100
for testFailureStage in range(3, 21):
101-
FailIfNot(test.TestPaseOnly(ip=options.deviceAddress1,
102-
setuppin=20202021,
103-
nodeid=1),
101+
FailIfNot(await test.TestPaseOnly(ip=options.deviceAddress1,
102+
setuppin=20202021,
103+
nodeid=1),
104104
"Failed to establish PASE connection with device")
105-
FailIfNot(test.TestCommissionFailureOnReport(1, testFailureStage),
105+
FailIfNot(await test.TestCommissionFailureOnReport(1, testFailureStage),
106106
"Commissioning failure tests failed for simulated report failure on stage {}".format(testFailureStage))
107107

108108
else:
109109
for testFailureStage in range(3, 21):
110-
FailIfNot(test.TestPaseOnly(ip=options.deviceAddress1,
111-
setuppin=20202021,
112-
nodeid=1),
110+
FailIfNot(await test.TestPaseOnly(ip=options.deviceAddress1,
111+
setuppin=20202021,
112+
nodeid=1),
113113
"Failed to establish PASE connection with device")
114-
FailIfNot(test.TestCommissionFailure(1, testFailureStage),
114+
FailIfNot(await test.TestCommissionFailure(1, testFailureStage),
115115
"Commissioning failure tests failed for simulated stage failure on stage {}".format(testFailureStage))
116116

117117
# Ensure we can still commission for real
118-
FailIfNot(test.TestPaseOnly(ip=options.deviceAddress1,
119-
setuppin=20202021,
120-
nodeid=1),
118+
FailIfNot(await test.TestPaseOnly(ip=options.deviceAddress1,
119+
setuppin=20202021,
120+
nodeid=1),
121121
"Failed to establish PASE connection with device")
122-
FailIfNot(test.TestCommissionFailure(1, 0), "Failed to commission device")
122+
FailIfNot(await test.TestCommissionFailure(1, 0), "Failed to commission device")
123123

124124
logger.info("Testing on off cluster")
125-
FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=1,
126-
endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster")
125+
FailIfNot(await test.TestOnOffCluster(nodeid=1,
126+
endpoint=LIGHTING_ENDPOINT_ID), "Failed to test on off cluster")
127127

128128
timeoutTicker.stop()
129129

@@ -136,7 +136,7 @@ def main():
136136

137137
if __name__ == "__main__":
138138
try:
139-
main()
139+
asyncio.run(main())
140140
except Exception as ex:
141141
logger.exception(ex)
142142
TestFail("Exception occurred when running tests.")

src/controller/python/test/test_scripts/commissioning_test.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
GROUP_ID = 0
4848

4949

50-
def main():
50+
async def main():
5151
optParser = OptionParser()
5252
optParser.add_option(
5353
"-t",
@@ -133,22 +133,22 @@ def main():
133133

134134
if options.deviceAddress:
135135
logger.info("Testing commissioning (IP)")
136-
FailIfNot(test.TestCommissioning(ip=options.deviceAddress,
137-
setuppin=20202021,
138-
nodeid=options.nodeid),
136+
FailIfNot(await test.TestCommissioning(ip=options.deviceAddress,
137+
setuppin=20202021,
138+
nodeid=options.nodeid),
139139
"Failed to finish commissioning")
140140
elif options.setupPayload:
141141
logger.info("Testing commissioning (w/ Setup Payload)")
142-
FailIfNot(test.TestCommissioningWithSetupPayload(setupPayload=options.setupPayload,
143-
nodeid=options.nodeid,
144-
discoveryType=options.discoveryType),
142+
FailIfNot(await test.TestCommissioningWithSetupPayload(setupPayload=options.setupPayload,
143+
nodeid=options.nodeid,
144+
discoveryType=options.discoveryType),
145145
"Failed to finish commissioning")
146146
else:
147147
TestFail("Must provide device address or setup payload to commissioning the device")
148148

149149
logger.info("Testing on off cluster")
150-
FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=options.nodeid,
151-
endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster")
150+
FailIfNot(await test.TestOnOffCluster(nodeid=options.nodeid,
151+
endpoint=LIGHTING_ENDPOINT_ID), "Failed to test on off cluster")
152152

153153
FailIfNot(test.TestUsedTestCommissioner(),
154154
"Test commissioner check failed")
@@ -164,7 +164,7 @@ def main():
164164

165165
if __name__ == "__main__":
166166
try:
167-
main()
167+
asyncio.run(main())
168168
except Exception as ex:
169169
logger.exception(ex)
170170
TestFail("Exception occurred when running tests.")

src/controller/python/test/test_scripts/commissioning_window_test.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ async def main():
8989
"Failed to finish network commissioning")
9090

9191
logger.info("Commissioning DUT from first commissioner")
92-
FailIfNot(test.TestPaseOnly(ip=options.deviceAddress, setuppin=20202021, nodeid=1),
92+
FailIfNot(await test.TestPaseOnly(ip=options.deviceAddress, setuppin=20202021, nodeid=1),
9393
"Unable to establish PASE connection to device")
94-
FailIfNot(test.TestCommissionOnly(nodeid=1), "Unable to commission device")
94+
FailIfNot(await test.TestCommissionOnly(nodeid=1), "Unable to commission device")
9595

9696
logger.info("Creating controller on a new fabric")
9797
FailIfNot(test.CreateNewFabricController(), "Unable to create new controller")
@@ -103,7 +103,7 @@ async def main():
103103
"RevokeCommissioning test failed")
104104

105105
logger.info("Test Enhanced Commissioning Window")
106-
FailIfNot(test.TestEnhancedCommissioningWindow(ip=options.deviceAddress, nodeid=1), "EnhancedCommissioningWindow open failed")
106+
FailIfNot(await test.TestEnhancedCommissioningWindow(ip=options.deviceAddress, nodeid=1), "EnhancedCommissioningWindow open failed")
107107

108108
timeoutTicker.stop()
109109

0 commit comments

Comments
 (0)