Skip to content

Commit

Permalink
Add TestOnlySendBatchCommands to chip-repl (#31455)
Browse files Browse the repository at this point in the history
* Add TestOnlySendBatchCommands to chip-repl

This allows chip-repl to send spec violating InvokeRequest for testing
purposes.

* Restyled by clang-format

* Restyled by autopep8

* Restyled by isort

* Fix CI

* Address PR comment

---------

Co-authored-by: Restyled.io <commits@restyled.io>
  • Loading branch information
2 people authored and pull[bot] committed Feb 22, 2024
1 parent 7542c48 commit 4935811
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 100 deletions.
30 changes: 29 additions & 1 deletion src/controller/python/chip/ChipDeviceCtrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,34 @@ def GetRemoteSessionParameters(self, nodeid) -> typing.Optional[SessionParameter

return res

async def TestOnlySendBatchCommands(self, nodeid: int, commands: typing.List[ClusterCommand.InvokeRequestInfo],
timedRequestTimeoutMs: typing.Optional[int] = None,
interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None,
suppressResponse: typing.Optional[bool] = None, remoteMaxPathsPerInvoke: typing.Optional[int] = None,
suppressTimedRequestMessage: bool = False, commandRefsOverride: typing.Optional[typing.List[int]] = None):
'''
Please see SendBatchCommands for description.
TestOnly overridable arguments:
remoteMaxPathsPerInvoke: Overrides the number of batch commands we think can be sent to remote node.
suppressTimedRequestMessage: When set to true, we suppress sending Timed Request Message.
commandRefsOverride: List of commandRefs to use for each command with the same index in `commands`.
'''
self.CheckIsActive()

eventLoop = asyncio.get_running_loop()
future = eventLoop.create_future()

device = self.GetConnectedDeviceSync(nodeid, timeoutMs=interactionTimeoutMs)

ClusterCommand.TestOnlySendBatchCommands(
future, eventLoop, device.deviceProxy, commands,
timedRequestTimeoutMs=timedRequestTimeoutMs,
interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse,
remoteMaxPathsPerInvoke=remoteMaxPathsPerInvoke, suppressTimedRequestMessage=suppressTimedRequestMessage,
commandRefsOverride=commandRefsOverride).raise_on_error()
return await future

async def TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(self, nodeid: int, endpoint: int,
payload: ClusterObjects.ClusterCommand, responseType=None):
'''
Expand Down Expand Up @@ -927,7 +955,7 @@ async def SendBatchCommands(self, nodeid: int, commands: typing.List[ClusterComm
- A value of `None` indicates success.
- If only a single command fails, for example with `UNSUPPORTED_COMMAND`, the corresponding index associated with the command will,
contain `interaction_model.Status.UnsupportedCommand`.
- If a command is not responded to by server, command will contain `interaction_model.Status.Failure`
- If a command is not responded to by server, command will contain `interaction_model.Status.NoCommandResponse`
Raises:
- InteractionModelError if error with sending of InvokeRequestMessage fails as a whole.
'''
Expand Down
110 changes: 88 additions & 22 deletions src/controller/python/chip/clusters/Command.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

import chip.exceptions
import chip.interaction_model
from chip.interaction_model import PyInvokeRequestData
from chip.interaction_model import PyInvokeRequestData, TestOnlyPyBatchCommandsOverrides
from chip.native import PyChipError

from .ClusterObjects import ClusterCommand
Expand Down Expand Up @@ -204,7 +204,9 @@ def handleError(self, status: Status, chipError: PyChipError):
)

def _handleDone(self):
self._future.set_result(self._responses)
# Future might already be set with exception from `handleError`
if not self._future.done():
self._future.set_result(self._responses)
ctypes.pythonapi.Py_DecRef(ctypes.py_object(self))

def handleDone(self):
Expand Down Expand Up @@ -296,24 +298,7 @@ def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPa
))


def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRequestInfo],
timedRequestTimeoutMs: Optional[int] = None, interactionTimeoutMs: Optional[int] = None, busyWaitMs: Optional[int] = None,
suppressResponse: Optional[bool] = None) -> PyChipError:
''' Send a cluster-object encapsulated command to a device and does the following:
- On receipt of a successful data response, returns the cluster-object equivalent through the provided future.
- None (on a successful response containing no data)
- Raises an exception if any errors are encountered.
If no response type is provided above, the type will be automatically deduced.
If a valid timedRequestTimeoutMs is provided, a timed interaction will be initiated instead.
If a valid interactionTimeoutMs is provided, the interaction will terminate with a CHIP_ERROR_TIMEOUT if a response
has not been received within that timeout. If it isn't provided, a sensible value will be automatically computed that
accounts for the underlying characteristics of both the transport and the responsiveness of the receiver.
'''
handle = chip.native.GetLibraryHandle()

responseTypes = []
def _BuildPyInvokeRequestData(commands: List[InvokeRequestInfo], timedRequestTimeoutMs: Optional[int], responseTypes, suppressTimedRequestMessage: bool = False) -> List[PyInvokeRequestData]:
numberOfCommands = len(commands)
pyBatchCommandsDataArrayType = PyInvokeRequestData * numberOfCommands
pyBatchCommandsData = pyBatchCommandsDataArrayType()
Expand All @@ -323,7 +308,8 @@ def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRe
if (responseType is not None) and (not issubclass(responseType, ClusterCommand)):
raise ValueError("responseType must be a ClusterCommand or None")
if clusterCommand.must_use_timed_invoke and timedRequestTimeoutMs is None or timedRequestTimeoutMs == 0:
raise chip.interaction_model.InteractionModelError(chip.interaction_model.Status.NeedsTimedInteraction)
if not suppressTimedRequestMessage:
raise chip.interaction_model.InteractionModelError(chip.interaction_model.Status.NeedsTimedInteraction)

payloadTLV = clusterCommand.ToTLV()

Expand All @@ -335,6 +321,41 @@ def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRe

responseTypes.append(responseType)

return pyBatchCommandsData


def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRequestInfo],
timedRequestTimeoutMs: Optional[int] = None, interactionTimeoutMs: Optional[int] = None, busyWaitMs: Optional[int] = None,
suppressResponse: Optional[bool] = None) -> PyChipError:
''' Initiates an InvokeInteraction with the batch commands provided.
Arguments:
- timedRequestTimeoutMs: If a valid value is provided, a timed interaction will be initiated.
- interactionTimeoutMs: If a valid value is provided, the interaction will terminate with a
CHIP_ERROR_TIMEOUT if a response is not received within the specified timeout. If not provided,
a suitable value will be automatically computed based on transport characteristics and
receiver responsiveness.
Returns:
- PyChipError: Indicates the outcome of initiating the InvokeRequest. Upon success the caller
is expected to await on `future` to get result of the InvokeInteraction.
Results passed via the provided future:
- Successful InvokeInteraction with path-specific responses (including path-specific errors):
- A list of responses is returned in the same order as the `commands` argument.
- Possible response elements:
- `None`: Successful command execution without additional cluster data.
- Encapsulated cluster-object: Successful command with response data.
- interaction_model.Status.*: Command failure with IM Status.
- interaction_model.Status.NoCommandResponse: No response from the server for
a specific command.
- Non-path-specific error: An `InteractionModelError` exception is raised through the future.
'''
handle = chip.native.GetLibraryHandle()

responseTypes = []
pyBatchCommandsData = _BuildPyInvokeRequestData(commands, timedRequestTimeoutMs, responseTypes)

transaction = AsyncBatchCommandsTransaction(future, eventLoop, responseTypes)
ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction))

Expand All @@ -345,7 +366,50 @@ def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRe
c_uint16(0 if interactionTimeoutMs is None else interactionTimeoutMs),
c_uint16(0 if busyWaitMs is None else busyWaitMs),
c_bool(False if suppressResponse is None else suppressResponse),
pyBatchCommandsData, c_size_t(numberOfCommands))
pyBatchCommandsData, c_size_t(len(pyBatchCommandsData)))
)


def TestOnlySendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRequestInfo],
timedRequestTimeoutMs: Optional[int] = None, interactionTimeoutMs: Optional[int] = None, busyWaitMs: Optional[int] = None,
suppressResponse: Optional[bool] = None, remoteMaxPathsPerInvoke: Optional[int] = None,
suppressTimedRequestMessage: bool = False, commandRefsOverride: Optional[List[int]] = None) -> PyChipError:
''' ONLY TO BE USED FOR TEST: Send batch commands using various overrides.
'''
if suppressTimedRequestMessage and timedRequestTimeoutMs is not None:
raise ValueError("timedRequestTimeoutMs has non-None value while suppressTimedRequestMessage")

overrideCommandRefs = None
if commandRefsOverride is not None:
if len(commandRefsOverride) != len(commands):
raise ValueError("Mismatch in the number of elements provided in commandRefsOverride")
overrideCommandRefsType = c_uint16 * len(commandRefsOverride)
overrideCommandRefs = overrideCommandRefsType()

handle = chip.native.GetLibraryHandle()

responseTypes = []
pyBatchCommandsData = _BuildPyInvokeRequestData(commands, timedRequestTimeoutMs,
responseTypes, suppressTimedRequestMessage=suppressTimedRequestMessage)

transaction = AsyncBatchCommandsTransaction(future, eventLoop, responseTypes)
ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction))

testOnlyOverrides = TestOnlyPyBatchCommandsOverrides()
testOnlyOverrides.suppressTimedRequestMessage = suppressTimedRequestMessage
testOnlyOverrides.overrideRemoteMaxPathsPerInvoke = 0 if remoteMaxPathsPerInvoke is None else c_uint16(remoteMaxPathsPerInvoke)
testOnlyOverrides.overrideCommandRefsList = overrideCommandRefs
testOnlyOverrides.overrideCommandRefsListLength = 0 if overrideCommandRefs is None else c_size_t(len(overrideCommandRefs))

return builtins.chipStack.Call(
lambda: handle.pychip_CommandSender_TestOnlySendBatchCommands(
py_object(transaction), device,
c_uint16(0 if timedRequestTimeoutMs is None else timedRequestTimeoutMs),
c_uint16(0 if interactionTimeoutMs is None else interactionTimeoutMs),
c_uint16(0 if busyWaitMs is None else busyWaitMs),
c_bool(False if suppressResponse is None else suppressResponse),
testOnlyOverrides,
pyBatchCommandsData, c_size_t(len(pyBatchCommandsData)))
)


Expand Down Expand Up @@ -377,6 +441,8 @@ def Init():
PyChipError, [py_object, c_void_p, c_uint16, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16, c_bool])
setter.Set('pychip_CommandSender_SendBatchCommands',
PyChipError, [py_object, c_void_p, c_uint16, c_uint16, c_uint16, c_bool, POINTER(PyInvokeRequestData), c_size_t])
setter.Set('pychip_CommandSender_TestOnlySendBatchCommands',
PyChipError, [py_object, c_void_p, c_uint16, c_uint16, c_uint16, c_bool, TestOnlyPyBatchCommandsOverrides, POINTER(PyInvokeRequestData), c_size_t])
setter.Set('pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke',
PyChipError, [py_object, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16, c_bool])
setter.Set('pychip_CommandSender_SendGroupCommand',
Expand Down
Loading

0 comments on commit 4935811

Please sign in to comment.