TC-IDM-1.2 (#27024)
* TC-IDM-1.2
Adds automation for TC-IDM-1.2
Also Adds suppressResponse to CommandSender as well as a test-only
function to test timedResponse flag with no corresponding
TimedInvoke action + plumbing through the python layers
* Restyled by isort
* Updates from review comments
* Couple formatting fixes
* Cleanup.
* Add a port to pase in python, fix filtering
* Consolidate CommandSender functions
* Timed invoke can be inferred
---------
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index b8e5afb..78c4070 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -448,6 +448,7 @@
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_decode 1" --script "src/python_testing/TC_CGEN_2_4.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_decode 1" --script "src/python_testing/TC_DA_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values"'
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_decode 1" --script "src/python_testing/TC_DA_1_5.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values"'
+ scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_decode 1" --script "src/python_testing/TC_IDM_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestMatterTestingSupport.py"'
- name: Uploading core files
uses: actions/upload-artifact@v3
diff --git a/config/python/CHIPProjectConfig.h b/config/python/CHIPProjectConfig.h
index 87940a9..5effaaa 100644
--- a/config/python/CHIPProjectConfig.h
+++ b/config/python/CHIPProjectConfig.h
@@ -59,4 +59,6 @@
#define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY 1
#define CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE 1
+#define CONFIG_BUILD_FOR_HOST_UNIT_TEST 1
+
#endif /* CHIPPROJECTCONFIG_H */
diff --git a/src/app/CommandSender.cpp b/src/app/CommandSender.cpp
index 5282f10..5daceaf 100644
--- a/src/app/CommandSender.cpp
+++ b/src/app/CommandSender.cpp
@@ -32,9 +32,10 @@
namespace chip {
namespace app {
-CommandSender::CommandSender(Callback * apCallback, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest) :
- mExchangeCtx(*this), mpCallback(apCallback), mpExchangeMgr(apExchangeMgr), mSuppressResponse(false),
- mTimedRequest(aIsTimedRequest)
+CommandSender::CommandSender(Callback * apCallback, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest,
+ bool aSuppressResponse) :
+ mExchangeCtx(*this),
+ mpCallback(apCallback), mpExchangeMgr(apExchangeMgr), mSuppressResponse(aSuppressResponse), mTimedRequest(aIsTimedRequest)
{}
CHIP_ERROR CommandSender::AllocateBuffer()
@@ -61,7 +62,7 @@
return CHIP_NO_ERROR;
}
-CHIP_ERROR CommandSender::SendCommandRequest(const SessionHandle & session, Optional<System::Clock::Timeout> timeout)
+CHIP_ERROR CommandSender::SendCommandRequestInternal(const SessionHandle & session, Optional<System::Clock::Timeout> timeout)
{
VerifyOrReturnError(mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE);
@@ -76,15 +77,6 @@
mExchangeCtx->SetResponseTimeout(timeout.ValueOr(session->ComputeRoundTripTimeout(app::kExpectedIMProcessingTime)));
- if (mTimedRequest != mTimedInvokeTimeoutMs.HasValue())
- {
- ChipLogError(
- DataManagement,
- "Inconsistent timed request state in CommandSender: mTimedRequest (%d) != mTimedInvokeTimeoutMs.HasValue() (%d)",
- mTimedRequest, mTimedInvokeTimeoutMs.HasValue());
- return CHIP_ERROR_INCORRECT_STATE;
- }
-
if (mTimedInvokeTimeoutMs.HasValue())
{
ReturnErrorOnFailure(TimedRequest::Send(mExchangeCtx.Get(), mTimedInvokeTimeoutMs.Value()));
@@ -95,6 +87,29 @@
return SendInvokeRequest();
}
+#if CONFIG_BUILD_FOR_HOST_UNIT_TEST
+CHIP_ERROR CommandSender::TestOnlyCommandSenderTimedRequestFlagWithNoTimedInvoke(const SessionHandle & session,
+ Optional<System::Clock::Timeout> timeout)
+{
+ VerifyOrReturnError(mTimedRequest, CHIP_ERROR_INCORRECT_STATE);
+ return SendCommandRequestInternal(session, timeout);
+}
+#endif
+
+CHIP_ERROR CommandSender::SendCommandRequest(const SessionHandle & session, Optional<System::Clock::Timeout> timeout)
+{
+
+ if (mTimedRequest != mTimedInvokeTimeoutMs.HasValue())
+ {
+ ChipLogError(
+ DataManagement,
+ "Inconsistent timed request state in CommandSender: mTimedRequest (%d) != mTimedInvokeTimeoutMs.HasValue() (%d)",
+ mTimedRequest, mTimedInvokeTimeoutMs.HasValue());
+ return CHIP_ERROR_INCORRECT_STATE;
+ }
+ return SendCommandRequestInternal(session, timeout);
+}
+
CHIP_ERROR CommandSender::SendGroupCommandRequest(const SessionHandle & session)
{
VerifyOrReturnError(mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE);
diff --git a/src/app/CommandSender.h b/src/app/CommandSender.h
index c61043c..6da9b05 100644
--- a/src/app/CommandSender.h
+++ b/src/app/CommandSender.h
@@ -121,7 +121,8 @@
* If used in a groups setting, callbacks do not need to be passed.
* If callbacks are passed the only one that will be called in a group sesttings is the onDone
*/
- CommandSender(Callback * apCallback, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest = false);
+ CommandSender(Callback * apCallback, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest = false,
+ bool aSuppressResponse = false);
CHIP_ERROR PrepareCommand(const CommandPathParams & aCommandPathParams, bool aStartDataStruct = true);
CHIP_ERROR FinishCommand(bool aEndDataStruct = true);
TLV::TLVWriter * GetCommandDataIBTLVWriter();
@@ -164,11 +165,18 @@
*/
template <typename CommandDataT>
CHIP_ERROR AddRequestDataNoTimedCheck(const CommandPathParams & aCommandPath, const CommandDataT & aData,
- const Optional<uint16_t> & aTimedInvokeTimeoutMs, bool aSuppressResponse = false)
+ const Optional<uint16_t> & aTimedInvokeTimeoutMs)
{
- mSuppressResponse = aSuppressResponse;
return AddRequestDataInternal(aCommandPath, aData, aTimedInvokeTimeoutMs);
}
+
+ /**
+ * Version of SendCommandRequest that sets the TimedRequest flag but does not send the TimedInvoke
+ * action. For use in tests only.
+ */
+ CHIP_ERROR TestOnlyCommandSenderTimedRequestFlagWithNoTimedInvoke(const SessionHandle & session,
+ Optional<System::Clock::Timeout> timeout = NullOptional);
+
#endif // CONFIG_BUILD_FOR_HOST_UNIT_TEST
private:
@@ -265,6 +273,8 @@
CHIP_ERROR Finalize(System::PacketBufferHandle & commandPacket);
+ CHIP_ERROR SendCommandRequestInternal(const SessionHandle & session, Optional<System::Clock::Timeout> timeout);
+
Messaging::ExchangeHolder mExchangeCtx;
Callback * mpCallback = nullptr;
Messaging::ExchangeManager * mpExchangeMgr = nullptr;
diff --git a/src/app/tests/suites/commands/interaction_model/InteractionModel.h b/src/app/tests/suites/commands/interaction_model/InteractionModel.h
index 1d3e2eb..846dd6a 100644
--- a/src/app/tests/suites/commands/interaction_model/InteractionModel.h
+++ b/src/app/tests/suites/commands/interaction_model/InteractionModel.h
@@ -234,12 +234,11 @@
chip::app::CommandPathParams commandPath = { endpointId, clusterId, commandId,
(chip::app::CommandPathFlags::kEndpointIdValid) };
- auto commandSender = std::make_unique<chip::app::CommandSender>(mCallback, device->GetExchangeManager(),
- mTimedInteractionTimeoutMs.HasValue());
+ auto commandSender = std::make_unique<chip::app::CommandSender>(
+ mCallback, device->GetExchangeManager(), mTimedInteractionTimeoutMs.HasValue(), mSuppressResponse.ValueOr(false));
VerifyOrReturnError(commandSender != nullptr, CHIP_ERROR_NO_MEMORY);
- ReturnErrorOnFailure(commandSender->AddRequestDataNoTimedCheck(commandPath, value, mTimedInteractionTimeoutMs,
- mSuppressResponse.ValueOr(false)));
+ ReturnErrorOnFailure(commandSender->AddRequestDataNoTimedCheck(commandPath, value, mTimedInteractionTimeoutMs));
ReturnErrorOnFailure(commandSender->SendCommandRequest(device->GetSecureSession().Value()));
mCommandSender.push_back(std::move(commandSender));
diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
index 39d4fa5..e69ad2d 100644
--- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp
+++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
@@ -139,7 +139,7 @@
PyChipError pychip_DeviceController_SetWiFiCredentials(const char * ssid, const char * credentials);
PyChipError pychip_DeviceController_CloseSession(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid);
PyChipError pychip_DeviceController_EstablishPASESessionIP(chip::Controller::DeviceCommissioner * devCtrl, const char * peerAddrStr,
- uint32_t setupPINCode, chip::NodeId nodeid);
+ uint32_t setupPINCode, chip::NodeId nodeid, uint16_t port);
PyChipError pychip_DeviceController_EstablishPASESessionBLE(chip::Controller::DeviceCommissioner * devCtrl, uint32_t setupPINCode,
uint16_t discriminator, chip::NodeId nodeid);
PyChipError pychip_DeviceController_Commission(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid);
@@ -512,13 +512,17 @@
}
PyChipError pychip_DeviceController_EstablishPASESessionIP(chip::Controller::DeviceCommissioner * devCtrl, const char * peerAddrStr,
- uint32_t setupPINCode, chip::NodeId nodeid)
+ uint32_t setupPINCode, chip::NodeId nodeid, uint16_t port)
{
chip::Inet::IPAddress peerAddr;
chip::Transport::PeerAddress addr;
RendezvousParameters params = chip::RendezvousParameters().SetSetupPINCode(setupPINCode);
VerifyOrReturnError(chip::Inet::IPAddress::FromString(peerAddrStr, peerAddr), ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT));
addr.SetTransportType(chip::Transport::Type::kUdp).SetIPAddress(peerAddr);
+ if (port != 0)
+ {
+ addr.SetPort(port);
+ }
params.SetPeerAddress(addr).SetDiscriminator(0);
sPairingDelegate.SetExpectingPairingComplete(true);
return ToPyChipError(devCtrl->EstablishPASEConnection(nodeid, params));
diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py
index 1022182..37437ea 100644
--- a/src/controller/python/chip/ChipDeviceCtrl.py
+++ b/src/controller/python/chip/ChipDeviceCtrl.py
@@ -473,13 +473,13 @@
self.devCtrl, setupPinCode, discriminator, nodeid)
)
- def EstablishPASESessionIP(self, ipaddr: str, setupPinCode: int, nodeid: int):
+ def EstablishPASESessionIP(self, ipaddr: str, setupPinCode: int, nodeid: int, port: int = 0):
self.CheckIsActive()
self.state = DCState.RENDEZVOUS_ONGOING
return self._ChipStack.CallAsync(
lambda: self._dmLib.pychip_DeviceController_EstablishPASESessionIP(
- self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid)
+ self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid, port)
)
def GetTestCommissionerUsed(self):
@@ -779,9 +779,30 @@
device.deviceProxy, upperLayerProcessingTimeoutMs))
return res
+ async def TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(self, nodeid: int, endpoint: int,
+ payload: ClusterObjects.ClusterCommand, responseType=None):
+ '''
+
+ Please see SendCommand for description.
+ '''
+ self.CheckIsActive()
+
+ eventLoop = asyncio.get_running_loop()
+ future = eventLoop.create_future()
+
+ device = self.GetConnectedDeviceSync(nodeid, timeoutMs=None)
+ ClusterCommand.TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(
+ future, eventLoop, responseType, device.deviceProxy, ClusterCommand.CommandPath(
+ EndpointId=endpoint,
+ ClusterId=payload.cluster_id,
+ CommandId=payload.command_id,
+ ), payload).raise_on_error()
+ return await future
+
async def SendCommand(self, nodeid: int, endpoint: int, payload: ClusterObjects.ClusterCommand, responseType=None,
timedRequestTimeoutMs: typing.Union[None, int] = None,
- interactionTimeoutMs: typing.Union[None, int] = None, busyWaitMs: typing.Union[None, int] = None):
+ interactionTimeoutMs: typing.Union[None, int] = None, busyWaitMs: typing.Union[None, int] = None,
+ suppressResponse: typing.Union[None, bool] = None):
'''
Send a cluster-object encapsulated command to a node and get returned a future that can be awaited upon to receive
the response. If a valid responseType is passed in, that will be used to deserialize the object. If not,
@@ -803,7 +824,7 @@
ClusterId=payload.cluster_id,
CommandId=payload.command_id,
), payload, timedRequestTimeoutMs=timedRequestTimeoutMs,
- interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs).raise_on_error()
+ interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse).raise_on_error()
return await future
def SendGroupCommand(self, groupid: int, payload: ClusterObjects.ClusterCommand, busyWaitMs: typing.Union[None, int] = None):
@@ -1338,7 +1359,7 @@
self._dmLib.pychip_DeviceController_DiscoverCommissionableNodesCommissioningEnabled.restype = PyChipError
self._dmLib.pychip_DeviceController_EstablishPASESessionIP.argtypes = [
- c_void_p, c_char_p, c_uint32, c_uint64]
+ c_void_p, c_char_p, c_uint32, c_uint64, c_uint16]
self._dmLib.pychip_DeviceController_EstablishPASESessionIP.restype = PyChipError
self._dmLib.pychip_DeviceController_EstablishPASESessionBLE.argtypes = [
diff --git a/src/controller/python/chip/clusters/Command.py b/src/controller/python/chip/clusters/Command.py
index 203c92a..af13365 100644
--- a/src/controller/python/chip/clusters/Command.py
+++ b/src/controller/python/chip/clusters/Command.py
@@ -21,7 +21,7 @@
import logging
import sys
from asyncio.futures import Future
-from ctypes import CFUNCTYPE, c_char_p, c_size_t, c_uint8, c_uint16, c_uint32, c_void_p, py_object
+from ctypes import CFUNCTYPE, c_bool, c_char_p, c_size_t, c_uint8, c_uint16, c_uint32, c_void_p, py_object
from dataclasses import dataclass
from typing import Type, Union
@@ -144,8 +144,30 @@
ctypes.pythonapi.Py_DecRef(ctypes.py_object(closure))
+def TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(future: Future, eventLoop, responseType, device, commandPath, payload):
+ ''' ONLY TO BE USED FOR TEST: Sends the payload with a TimedRequest flag but no TimedInvoke transaction
+ '''
+ if (responseType is not None) and (not issubclass(responseType, ClusterCommand)):
+ raise ValueError("responseType must be a ClusterCommand or None")
+
+ handle = chip.native.GetLibraryHandle()
+ transaction = AsyncCommandTransaction(future, eventLoop, responseType)
+
+ payloadTLV = payload.ToTLV()
+ ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction))
+ return builtins.chipStack.Call(
+ lambda: handle.pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke(
+ ctypes.py_object(transaction), device,
+ commandPath.EndpointId, commandPath.ClusterId, commandPath.CommandId, payloadTLV, len(payloadTLV),
+ ctypes.c_uint16(0), # interactionTimeoutMs
+ ctypes.c_uint16(0), # busyWaitMs
+ ctypes.c_bool(False) # suppressResponse
+ ))
+
+
def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPath: CommandPath, payload: ClusterCommand,
- timedRequestTimeoutMs: Union[None, int] = None, interactionTimeoutMs: Union[None, int] = None, busyWaitMs: Union[None, int] = None) -> PyChipError:
+ timedRequestTimeoutMs: Union[None, int] = None, interactionTimeoutMs: Union[None, int] = None, busyWaitMs: Union[None, int] = None,
+ suppressResponse: Union[None, 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)
@@ -175,6 +197,7 @@
commandPath.ClusterId, commandPath.CommandId, payloadTLV, len(payloadTLV),
ctypes.c_uint16(0 if interactionTimeoutMs is None else interactionTimeoutMs),
ctypes.c_uint16(0 if busyWaitMs is None else busyWaitMs),
+ ctypes.c_bool(False if suppressResponse is None else suppressResponse)
))
@@ -203,7 +226,9 @@
setter = chip.native.NativeLibraryHandleMethodArguments(handle)
setter.Set('pychip_CommandSender_SendCommand',
- PyChipError, [py_object, c_void_p, c_uint16, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16])
+ 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_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',
PyChipError, [c_uint16, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16])
setter.Set('pychip_CommandSender_InitCallbacks', None, [
diff --git a/src/controller/python/chip/clusters/command.cpp b/src/controller/python/chip/clusters/command.cpp
index 468bff5..c824c25 100644
--- a/src/controller/python/chip/clusters/command.cpp
+++ b/src/controller/python/chip/clusters/command.cpp
@@ -36,7 +36,11 @@
PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * device, uint16_t timedRequestTimeoutMs,
chip::EndpointId endpointId, chip::ClusterId clusterId, chip::CommandId commandId,
const uint8_t * payload, size_t length, uint16_t interactionTimeoutMs,
- uint16_t busyWaitMs);
+ uint16_t busyWaitMs, bool suppressResponse);
+
+PyChipError pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke(
+ void * appContext, DeviceProxy * device, chip::EndpointId endpointId, chip::ClusterId clusterId, chip::CommandId commandId,
+ const uint8_t * payload, size_t length, uint16_t interactionTimeoutMs, uint16_t busyWaitMs, bool suppressResponse);
PyChipError pychip_CommandSender_SendGroupCommand(chip::GroupId groupId, chip::Controller::DeviceCommissioner * devCtrl,
chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload,
@@ -132,15 +136,16 @@
PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * device, uint16_t timedRequestTimeoutMs,
chip::EndpointId endpointId, chip::ClusterId clusterId, chip::CommandId commandId,
const uint8_t * payload, size_t length, uint16_t interactionTimeoutMs,
- uint16_t busyWaitMs)
+ uint16_t busyWaitMs, bool suppressResponse)
{
CHIP_ERROR err = CHIP_NO_ERROR;
VerifyOrReturnError(device->GetSecureSession().HasValue(), ToPyChipError(CHIP_ERROR_MISSING_SECURE_SESSION));
std::unique_ptr<CommandSenderCallback> callback = std::make_unique<CommandSenderCallback>(appContext);
- std::unique_ptr<CommandSender> sender = std::make_unique<CommandSender>(callback.get(), device->GetExchangeManager(),
- /* is timed request */ timedRequestTimeoutMs != 0);
+ std::unique_ptr<CommandSender> sender =
+ std::make_unique<CommandSender>(callback.get(), device->GetExchangeManager(),
+ /* is timed request */ timedRequestTimeoutMs != 0, suppressResponse);
app::CommandPathParams cmdParams = { endpointId, /* group id */ 0, clusterId, commandId,
(app::CommandPathFlags::kEndpointIdValid) };
@@ -176,6 +181,56 @@
return ToPyChipError(err);
}
+PyChipError pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke(
+ void * appContext, DeviceProxy * device, chip::EndpointId endpointId, chip::ClusterId clusterId, chip::CommandId commandId,
+ const uint8_t * payload, size_t length, uint16_t interactionTimeoutMs, uint16_t busyWaitMs, bool suppressResponse)
+{
+#if CONFIG_BUILD_FOR_HOST_UNIT_TEST
+
+ CHIP_ERROR err = CHIP_NO_ERROR;
+
+ VerifyOrReturnError(device->GetSecureSession().HasValue(), ToPyChipError(CHIP_ERROR_MISSING_SECURE_SESSION));
+
+ std::unique_ptr<CommandSenderCallback> callback = std::make_unique<CommandSenderCallback>(appContext);
+ std::unique_ptr<CommandSender> sender = std::make_unique<CommandSender>(callback.get(), device->GetExchangeManager(),
+ /* is timed request */ true, suppressResponse);
+
+ app::CommandPathParams cmdParams = { endpointId, /* group id */ 0, clusterId, commandId,
+ (app::CommandPathFlags::kEndpointIdValid) };
+
+ SuccessOrExit(err = sender->PrepareCommand(cmdParams, false));
+
+ {
+ auto writer = sender->GetCommandDataIBTLVWriter();
+ TLV::TLVReader reader;
+ VerifyOrExit(writer != nullptr, err = CHIP_ERROR_INCORRECT_STATE);
+ reader.Init(payload, length);
+ reader.Next();
+ SuccessOrExit(err = writer->CopyContainer(TLV::ContextTag(CommandDataIB::Tag::kFields), reader));
+ }
+
+ SuccessOrExit(err = sender->FinishCommand(false));
+
+ SuccessOrExit(err = sender->TestOnlyCommandSenderTimedRequestFlagWithNoTimedInvoke(
+ device->GetSecureSession().Value(),
+ interactionTimeoutMs != 0 ? MakeOptional(System::Clock::Milliseconds32(interactionTimeoutMs))
+ : Optional<System::Clock::Timeout>::Missing()));
+
+ sender.release();
+ callback.release();
+
+ if (busyWaitMs)
+ {
+ usleep(busyWaitMs * 1000);
+ }
+
+exit:
+ return ToPyChipError(err);
+#else
+ return ToPyChipError(CHIP_ERROR_NOT_IMPLEMENTED);
+#endif
+}
+
PyChipError pychip_CommandSender_SendGroupCommand(chip::GroupId groupId, chip::Controller::DeviceCommissioner * devCtrl,
chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload,
size_t length, uint16_t busyWaitMs)
diff --git a/src/python_testing/TC_IDM_1_2.py b/src/python_testing/TC_IDM_1_2.py
new file mode 100644
index 0000000..7adb7eb
--- /dev/null
+++ b/src/python_testing/TC_IDM_1_2.py
@@ -0,0 +1,285 @@
+#
+# Copyright (c) 2023 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 inspect
+import logging
+from dataclasses import dataclass
+
+import chip.clusters as Clusters
+import chip.discovery as Discovery
+from chip import ChipUtility
+from chip.exceptions import ChipStackError
+from chip.interaction_model import InteractionModelError, Status
+from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main, type_matches
+from mobly import asserts
+
+
+def get_all_cmds_for_cluster_id(cid: int) -> list[Clusters.ClusterObjects.ClusterCommand]:
+ cluster = Clusters.ClusterObjects.ALL_CLUSTERS[cid]
+ try:
+ return inspect.getmembers(cluster.Commands, inspect.isclass)
+ except AttributeError:
+ return []
+
+
+def client_cmd(cmd_class):
+ # Inspect returns all the classes, not just the ones we want, so use a try
+ # here incase we're inspecting a builtin class
+ try:
+ return cmd_class if cmd_class.is_client else None
+ except AttributeError:
+ return None
+
+# one of the steps in this test requires sending a command that requires a timed interaction
+# without first sending the TimedRequest action
+# OpenCommissioningWindow requires a timed invoke and is mandatory on servers, BUT, it's marked
+# that way in the base class. We need a new, fake class that doesn't have that set
+
+
+@dataclass
+class FakeRevokeCommissioning(Clusters.AdministratorCommissioning.Commands.RevokeCommissioning):
+ @ChipUtility.classproperty
+ def must_use_timed_invoke(cls) -> bool:
+ return False
+
+
+class TC_IDM_1_2(MatterBaseTest):
+
+ @async_test_body
+ async def test_TC_IDM_1_2(self):
+ self.print_step(0, "Commissioning - already done")
+ wildcard_descriptor = await self.default_controller.ReadAttribute(self.dut_node_id, [(Clusters.Descriptor)])
+ endpoints = list(wildcard_descriptor.keys())
+ endpoints.sort()
+
+ self.print_step(1, "Send Invoke to unsupported endpoint")
+ # First non-existent endpoint is where the index and and endpoint number don't match
+ non_existent_endpoint = next(i for i, e in enumerate(endpoints + [None]) if i != e)
+ # General Commissioning cluster should be supported on all DUTs, so it will recognize this cluster and
+ # command, but it is sent on an unsupported endpoint
+ cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=0, breadcrumb=1)
+ try:
+ await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=non_existent_endpoint, payload=cmd)
+ asserts.fail("Unexpected success return from sending command to unsupported endpoint")
+ except InteractionModelError as e:
+ asserts.assert_equal(e.status, Status.UnsupportedEndpoint, "Unexpected error returned from unsupported endpoint")
+
+ self.print_step(2, "Send Invoke to unsupported cluster")
+ all_cluster_ids = list(Clusters.ClusterObjects.ALL_CLUSTERS.keys())
+ unsupported_clusters: dict[int, list[int]] = {}
+ supported_clusters: dict[int, list[int]] = {}
+ for i in endpoints:
+ dut_ep_cluster_ids = wildcard_descriptor[i][Clusters.Descriptor][Clusters.Descriptor.Attributes.ServerList]
+ unsupported_clusters[i] = list(set(all_cluster_ids) - set(dut_ep_cluster_ids))
+ supported_clusters[i] = set(dut_ep_cluster_ids)
+
+ # This is really unlikely to happen on any real product, so we're going to assert here if we can't find anything
+ # since it's likely a test error
+ asserts.assert_true(any(unsupported_clusters[i] for i in endpoints),
+ "Unable to find any unsupported clusters on any endpoint")
+ asserts.assert_true(any(supported_clusters[i] for i in endpoints), "Unable to find supported clusters on any endpoint")
+
+ sent = False
+ for i in endpoints:
+ if sent:
+ break
+ for cid in unsupported_clusters[i]:
+ cluster = Clusters.ClusterObjects.ALL_CLUSTERS[cid]
+ members = get_all_cmds_for_cluster_id(cid)
+ if not members:
+ continue
+ # just use the first command with default values
+ name, cmd = members[0]
+ logging.info(f'Sending {name} command to unsupported cluster {cluster} on endpoint {i}')
+ try:
+ await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=i, payload=cmd())
+ asserts.fail("Unexpected success return from sending command to unsupported cluster")
+ except InteractionModelError as e:
+ asserts.assert_equal(e.status, Status.UnsupportedCluster, "Unexpected error returned from unsupported cluster")
+ sent = True
+ break
+ asserts.assert_true(sent, "Unable to find unsupported cluster with commands on any supported endpoint")
+
+ self.print_step(3, "Send Invoke for unsupported command")
+ # First read all the supported commands by wildcard reading the AcceptedCommands attribute from all clusters
+ # We can't wildcard across clusters even if the attribute is the same, so we're going to go 1 by 1.
+ # Just go endpoint by endpoint so we can early exit (each supports different clusters)
+ # TODO: add option to make this a beefier test that does all the commands?
+ sent = False
+ for i in endpoints:
+ if sent:
+ break
+ for cid in supported_clusters[i]:
+ cluster = Clusters.ClusterObjects.ALL_CLUSTERS[cid]
+ logging.info(f'Checking cluster {cluster} ({cid}) on ep {i} for supported commands')
+ members = get_all_cmds_for_cluster_id(cid)
+ if not members:
+ continue
+
+ dut_supported_ids = await self.read_single_attribute_check_success(cluster=cluster, endpoint=i, attribute=cluster.Attributes.AcceptedCommandList)
+ all_supported_cmds = list(filter(None, [client_cmd(x[1]) for x in members]))
+ all_supported_ids = [x.command_id for x in all_supported_cmds]
+ unsupported_commands = list(set(all_supported_ids) - set(dut_supported_ids))
+ if not unsupported_commands:
+ continue
+
+ # Let's just use the first unsupported command
+ id = unsupported_commands[0]
+ cmd = next(filter(lambda x: x.command_id == id, all_supported_cmds))
+ try:
+ ret = await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=i, payload=cmd())
+ asserts.fail(f'Unexpected success sending unsupported cmd {cmd} to {cluster} cluster on ep {i}')
+ except InteractionModelError as e:
+ asserts.assert_equal(e.status, Status.UnsupportedCommand, "Unexpected error returned from unsupported command")
+ sent = True
+ break
+
+ # It might actually be the case that all the supported clusters support all the commands. In that case, let's just put a warning.
+ # We could, in theory, send a command with a fully out of bounds command ID, but that's not supported by the controller
+ if not sent:
+ logging.warning("Unable to find a supported cluster with unsupported commands on any endpoint - SKIPPING")
+
+ self.print_step(4, "Setup TH to have no privileges for a cluster, send Invoke")
+ # Setup the ACL
+ acl_only = Clusters.AccessControl.Structs.AccessControlEntryStruct(
+ privilege=Clusters.AccessControl.Enums.AccessControlEntryPrivilegeEnum.kAdminister,
+ authMode=Clusters.AccessControl.Enums.AccessControlEntryAuthModeEnum.kCase,
+ subjects=[self.matter_test_config.controller_node_id],
+ targets=[Clusters.AccessControl.Structs.AccessControlTargetStruct(endpoint=0, cluster=Clusters.AccessControl.id)])
+ result = await self.default_controller.WriteAttribute(self.dut_node_id, [(0, Clusters.AccessControl.Attributes.Acl([acl_only]))])
+ asserts.assert_equal(result[0].Status, Status.Success, "ACL write failed")
+
+ # For the unsupported access test, let's use a cluster that's known to be there and supports commands - general commissioning on EP0
+ cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=0, breadcrumb=1)
+ try:
+ await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd)
+ asserts.fail("Unexpected success return when sending a command with no privileges")
+ except InteractionModelError as e:
+ asserts.assert_equal(e.status, Status.UnsupportedAccess, "Unexpected error returned")
+
+ full_access = Clusters.AccessControl.Structs.AccessControlEntryStruct(
+ privilege=Clusters.AccessControl.Enums.AccessControlEntryPrivilegeEnum.kAdminister,
+ authMode=Clusters.AccessControl.Enums.AccessControlEntryAuthModeEnum.kCase,
+ subjects=[self.matter_test_config.controller_node_id],
+ targets=[])
+ result = await self.default_controller.WriteAttribute(self.dut_node_id, [(0, Clusters.AccessControl.Attributes.Acl([full_access]))])
+ asserts.assert_equal(result[0].Status, Status.Success, "ACL write failed")
+
+ self.print_step(5, "setup TH with no accessing fabric and invoke command")
+ # The only way to have no accessing fabric is to have a PASE session and no added NOC
+ # KeySetRead - fabric scoped command, should not be accessible over PASE
+ # To get a PASE session, we need an open commissioning window
+ discriminator = self.matter_test_config.discriminators[0] + 1
+
+ params = self.default_controller.OpenCommissioningWindow(
+ nodeid=self.dut_node_id, timeout=600, iteration=10000, discriminator=discriminator, option=1)
+
+ # TH2 = new controller that's not connected over CASE
+ new_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority()
+ new_fabric_admin = new_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=self.matter_test_config.fabric_id + 1)
+ TH2 = new_fabric_admin.NewController(nodeId=112233)
+
+ devices = TH2.DiscoverCommissionableNodes(
+ filterType=Discovery.FilterType.LONG_DISCRIMINATOR, filter=discriminator, stopOnFirst=False)
+ # For some reason, the devices returned here aren't filtered, so filter ourselves
+ device = next(filter(lambda d: d.commissioningMode == 2 and d.longDiscriminator == discriminator, devices))
+ for a in device.addresses:
+ try:
+ TH2.EstablishPASESessionIP(ipaddr=a, setupPinCode=params.setupPinCode,
+ nodeid=self.dut_node_id+1, port=device.port)
+ break
+ except ChipStackError:
+ continue
+
+ try:
+ TH2.GetConnectedDeviceSync(nodeid=self.dut_node_id+1, allowPASE=True, timeoutMs=1000)
+ except TimeoutError:
+ asserts.fail("Unable to establish a PASE session to the device")
+
+ try:
+ # Any group ID is fine since we'll fail before this
+ await TH2.SendCommand(nodeid=self.dut_node_id + 1, endpoint=0, payload=Clusters.GroupKeyManagement.Commands.KeySetRead(groupKeySetID=0x0001))
+ asserts.fail("Incorrectly received a success response from a fabric-scoped command")
+ except InteractionModelError as e:
+ asserts.assert_equal(e.status, Status.UnsupportedAccess, "Incorrect error from fabric-sensitive read over PASE")
+
+ # Cleanup - RevokeCommissioning so we can use ArmFailSafe etc. again.
+ await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=Clusters.AdministratorCommissioning.Commands.RevokeCommissioning(), timedRequestTimeoutMs=6000)
+
+ self.print_step(6, "Send invoke request with requires a data response")
+ # ArmFailSafe sends a data response
+ cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=900, breadcrumb=1)
+ ret = await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd)
+ asserts.assert_true(type_matches(ret, Clusters.GeneralCommissioning.Commands.ArmFailSafeResponse),
+ "Unexpected response type from ArmFailSafe")
+
+ self.print_step(7, "Send a command with suppress Response")
+ # NOTE: This is out of scope currently due to https://github.com/project-chip/connectedhomeip/issues/8043
+ # We perform this step, but the DUT will likely incorrectly send a response
+ # Sending this command at least ensures the DUT doesn't crash with this flag set, even if the behvaior is not correct
+
+ # Lucky candidate ArmFailSafe is at it again - command side effect is to set breadcrumb attribute
+ cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=900, breadcrumb=2)
+ await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd, suppressResponse=True)
+ # TODO: Once the above issue is resolved, this needs a check to ensure that no response was received.
+
+ # Verify that the command had the correct side effect even if a response was sent
+ breadcrumb = await self.read_single_attribute_check_success(
+ cluster=Clusters.GeneralCommissioning, attribute=Clusters.GeneralCommissioning.Attributes.Breadcrumb, endpoint=0)
+ asserts.assert_equal(breadcrumb, 2, "Breadcrumb was not correctly set on ArmFailSafe with response suppressed")
+
+ # Cleanup - Unset the failsafe
+ cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=0, breadcrumb=0)
+ await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd)
+
+ self.print_step(8, "Send Invoke with timedRequest marked, but no timed request sent")
+ # We can do this with any command, but to be thorough, test first with a command that does not
+ # require a timed interaction (ArmFailSafe) and then one that does (RevokeCommissioning)
+ try:
+ await self.default_controller.TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(nodeid=self.dut_node_id, endpoint=0, payload=cmd)
+ asserts.fail("Unexpected success response from sending an Invoke with TimedRequest flag and no timed interaction")
+ except InteractionModelError as e:
+ asserts.assert_equal(e.status, Status.UnsupportedAccess,
+ "Unexpected error response from Invoke with TimedRequest flag and no TimedInvoke")
+
+ # Try with RevokeCommissioning
+ # First open a commissioning window for us to revoke, so we know this command is able to succeed absent this error
+ _ = self.default_controller.OpenCommissioningWindow(
+ nodeid=self.dut_node_id, timeout=600, iteration=10000, discriminator=1234, option=1)
+ cmd = FakeRevokeCommissioning()
+ try:
+ await self.default_controller.TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(nodeid=self.dut_node_id, endpoint=0, payload=cmd)
+ asserts.fail("Unexpected success response from sending an Invoke with TimedRequest flag and no timed interaction")
+ except InteractionModelError as e:
+ asserts.assert_equal(e.status, Status.UnsupportedAccess,
+ "Unexpected error response from Invoke with TimedRequest flag and no TimedInvoke")
+
+ self.print_step(9, "Send invoke for a command that requires timedRequest, but doesn't use one")
+ # RevokeCommissioning requires a timed interaction. This is enforced in the python layer because
+ # the generated class indicates that a timed interaction is required. The fake class overrides this.
+ try:
+ await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd)
+ asserts.fail("Incorrectly received a success response for a command that required TimedInvoke action")
+ except InteractionModelError as e:
+ asserts.assert_equal(e.status, Status.NeedsTimedInteraction)
+
+ # Cleanup - actually revoke commissioning to close the open window
+ await self.default_controller.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=Clusters.AdministratorCommissioning.Commands.RevokeCommissioning(), timedRequestTimeoutMs=6000)
+
+
+if __name__ == "__main__":
+ default_matter_test_main()