[WebRTCP] Implement test case for TC_WEBRTCP_2_12 & TC_WEBRTCP_2_16 (#41214)
* Implement test case for TC_WEBRTCP_2_12
* Address gemini review comments
* Fix python warnings
* Remove unused import
* Use lower case for varibles
diff --git a/examples/camera-app/linux/src/clusters/webrtc-provider/webrtc-provider-manager.cpp b/examples/camera-app/linux/src/clusters/webrtc-provider/webrtc-provider-manager.cpp
index 80cb982..692902a 100644
--- a/examples/camera-app/linux/src/clusters/webrtc-provider/webrtc-provider-manager.cpp
+++ b/examples/camera-app/linux/src/clusters/webrtc-provider/webrtc-provider-manager.cpp
@@ -32,6 +32,13 @@
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::WebRTCTransportProvider;
+namespace {
+
+// Constants
+constexpr uint16_t kMaxConcurrentWebRTCSessions = 5;
+
+} // namespace
+
void WebRTCProviderManager::SetCameraDevice(CameraDeviceInterface * aCameraDevice)
{
mCameraDevice = aCameraDevice;
@@ -134,6 +141,23 @@
}
transport->SetRequestArgs(requestArgs);
+
+ // Check resource availability before proceeding
+ // If we cannot allocate resources, send End command with OutOfResources reason
+ if (mWebrtcTransportMap.size() > kMaxConcurrentWebRTCSessions)
+ {
+ ChipLogProgress(Camera, "Resource exhaustion detected: maximum WebRTC sessions (%u)", kMaxConcurrentWebRTCSessions);
+
+ transport->SetCommandType(WebrtcTransport::CommandType::kEnd);
+ transport->MoveToState(WebrtcTransport::State::SendingEnd);
+
+ // The resource exhaustion happens internally in the DUT, but it still creates a session
+ // and then sends an End command with OutOfResources reason.
+ ScheduleEndSend(args.sessionId);
+
+ return CHIP_NO_ERROR;
+ }
+
transport->Start();
transport->AddAudioTrack();
transport->AddVideoTrack();
@@ -282,6 +306,15 @@
[this](bool connected, const uint16_t sessionId) { this->OnConnectionStateChanged(connected, sessionId); });
}
+ // Check resource availability before proceeding
+ // If we cannot allocate resources, respond with a response status of RESOURCE_EXHAUSTED
+ if (mWebrtcTransportMap.size() > kMaxConcurrentWebRTCSessions)
+ {
+ ChipLogProgress(Camera, "Resource exhaustion detected in ProvideOffer: maximum WebRTC sessions (%u)",
+ kMaxConcurrentWebRTCSessions);
+ return CHIP_IM_GLOBAL_STATUS(ResourceExhausted);
+ }
+
transport->SetRequestArgs(requestArgs);
transport->Start();
auto peerConnection = transport->GetPeerConnection();
diff --git a/src/python_testing/TC_WEBRTCP_2_12.py b/src/python_testing/TC_WEBRTCP_2_12.py
new file mode 100644
index 0000000..f6eff41
--- /dev/null
+++ b/src/python_testing/TC_WEBRTCP_2_12.py
@@ -0,0 +1,186 @@
+#
+# Copyright (c) 2025 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.
+#
+
+# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments
+# for details about the block below.
+#
+# === BEGIN CI TEST ARGUMENTS ===
+# test-runner-runs:
+# run1:
+# app: ${CAMERA_APP}
+# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# script-args: >
+# --PICS src/app/tests/suites/certification/ci-pics-values
+# --storage-path admin_storage.json
+# --commissioning-method on-network
+# --discriminator 1234
+# --passcode 20202021
+# --trace-to json:${TRACE_TEST_JSON}.json
+# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+# factory-reset: true
+# quiet: true
+# === END CI TEST ARGUMENTS ===
+#
+
+import logging
+
+from mobly import asserts
+from TC_WEBRTCPTestBase import WEBRTCPTestBase
+
+import matter.clusters as Clusters
+from matter import ChipDeviceCtrl
+from matter.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from matter.webrtc import LibdatachannelPeerConnection, WebRTCManager
+
+logger = logging.getLogger(__name__)
+
+
+class TC_WEBRTCP_2_12(MatterBaseTest, WEBRTCPTestBase):
+ def desc_TC_WEBRTCP_2_12(self) -> str:
+ """Returns a description of this test"""
+ return "[TC-WEBRTCP-2.12] Validate SolicitOffer resource exhaustion"
+
+ def steps_TC_WEBRTCP_2_12(self) -> list[TestStep]:
+ steps = [
+ TestStep(1, "TH allocates both Audio and Video streams via AudioStreamAllocate and VideoStreamAllocate commands to CameraAVStreamManagement",
+ "DUT responds with success and provides stream IDs"),
+ TestStep(2, "TH sends multiple SolicitOffer commands to exhaust the DUT's capacity for WebRTC sessions (DUT-specific limit)",
+ "DUT successfully creates sessions until capacity is reached"),
+ TestStep(3, "TH sends an additional SolicitOffer command when DUT capacity is exhausted",
+ "DUT responds with SolicitOfferResponse containing allocated WebRTCSessionID"),
+ TestStep(4, "TH waits for DUT to send End command with reason OutOfResources",
+ "DUT sends End command with reason OutOfResources (value 0x08)")
+ ]
+ return steps
+
+ def pics_TC_WEBRTCP_2_12(self) -> list[str]:
+ pics = [
+ "WEBRTCP.S",
+ "AVSM.S",
+ ]
+ return pics
+
+ @async_test_body
+ async def test_TC_WEBRTCP_2_12(self):
+ """
+ Executes the test steps for validating SolicitOffer resource exhaustion.
+ """
+
+ endpoint = self.get_endpoint(default=1)
+
+ self.step(1)
+ # Allocate Audio and Video streams
+ audio_stream_id = await self.allocate_one_audio_stream()
+ video_stream_id = await self.allocate_one_video_stream()
+
+ # Validate that the streams were allocated successfully
+ await self.validate_allocated_audio_stream(audio_stream_id)
+ await self.validate_allocated_video_stream(video_stream_id)
+
+ # Create WebRTC manager and peer for receiving End commands
+ webrtc_manager = WebRTCManager(event_loop=self.event_loop)
+ webrtc_peer: LibdatachannelPeerConnection = webrtc_manager.create_peer(
+ node_id=self.dut_node_id, fabric_index=self.default_controller.GetFabricIndexInternal(), endpoint=endpoint
+ )
+
+ self.step(2)
+ # Send multiple SolicitOffer commands to exhaust the DUT's capacity
+ logger.info("Starting to send SolicitOffer commands to exhaust DUT capacity")
+
+ # Get the maximum concurrent WebRTC sessions from user or use default for CI
+ prompt_msg = (
+ "\nPlease enter the maximum number of concurrent WebRTC sessions supported by the DUT:\n"
+ "This value is DUT-specific and should be obtained from the DUT documentation or specifications.\n"
+ "Enter the number (e.g., 5): "
+ )
+
+ if self.is_pics_sdk_ci_only:
+ # Use default value for CI testing
+ max_attempts = 5
+ else:
+ user_input = self.wait_for_user_input(prompt_msg)
+ try:
+ max_attempts = int(user_input.strip())
+ asserts.assert_true(max_attempts > 0, "Maximum concurrent sessions must be greater than 0")
+ logger.info(f"Using user-specified max_attempts={max_attempts}")
+ except ValueError:
+ asserts.fail(f"Invalid input '{user_input}'. Please enter a valid number.")
+
+ # Try to allocate multiple sessions to reach the DUT's capacity limit
+
+ for attempt in range(max_attempts):
+ logger.info(f"Attempt {attempt + 1}: Sending SolicitOffer command")
+ resp: Clusters.WebRTCTransportProvider.Commands.SolicitOfferResponse = await webrtc_peer.send_command(
+ cmd=Clusters.WebRTCTransportProvider.Commands.SolicitOffer(
+ streamUsage=Clusters.Objects.Globals.Enums.StreamUsageEnum.kLiveView,
+ videoStreamID=video_stream_id,
+ audioStreamID=audio_stream_id,
+ originatingEndpointID=1,
+ ),
+ endpoint=endpoint,
+ payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD,
+ )
+ asserts.assert_equal(type(resp), Clusters.WebRTCTransportProvider.Commands.SolicitOfferResponse,
+ "Incorrect response type")
+ session_id = resp.webRTCSessionID
+ asserts.assert_true(session_id >= 0, f"Invalid session ID: {session_id}")
+ logger.info(f"Created session {session_id} in attempt {attempt + 1}")
+ webrtc_manager.session_id_created(session_id, self.dut_node_id)
+
+ self.step(3)
+ # Send an additional SolicitOffer command when DUT capacity is exhausted
+ logger.info("Sending additional SolicitOffer command to trigger resource exhaustion")
+
+ # The resource exhaustion happens internally in the DUT, but it still creates a session
+ # and then sends an End command with OutOfResources reason.
+ resp: Clusters.WebRTCTransportProvider.Commands.SolicitOfferResponse = await webrtc_peer.send_command(
+ cmd=Clusters.WebRTCTransportProvider.Commands.SolicitOffer(
+ streamUsage=Clusters.Objects.Globals.Enums.StreamUsageEnum.kLiveView,
+ videoStreamID=video_stream_id,
+ audioStreamID=audio_stream_id,
+ originatingEndpointID=1,
+ ),
+ endpoint=endpoint,
+ payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD,
+ )
+
+ asserts.assert_equal(type(resp), Clusters.WebRTCTransportProvider.Commands.SolicitOfferResponse,
+ "Incorrect response type")
+ session_id = resp.webRTCSessionID
+ asserts.assert_true(session_id >= 0, f"Invalid session ID: {session_id}")
+ logger.info(f"DUT allocated session {session_id} despite resource exhaustion")
+ webrtc_manager.session_id_created(session_id, self.dut_node_id)
+
+ self.step(4)
+ # Wait for DUT to send End command with reason OutOfResources
+ logger.info("Waiting for DUT to send End command with OutOfResources reason")
+
+ # Wait for the End command from the DUT
+ end_sessionId, reason = await webrtc_peer.get_remote_end()
+
+ # Verify the End command has OutOfResources reason (value 0x08)
+ kOutOfResourcesReason = 0x08 # WebRTCEndReasonEnum.kOutOfResources
+ asserts.assert_equal(reason, kOutOfResourcesReason,
+ f"Expected OutOfResources reason ({kOutOfResourcesReason}), got {reason}")
+
+ logger.info(f"Successfully received End command for session {end_sessionId} with OutOfResources reason {reason}")
+
+ await webrtc_manager.close_all()
+
+
+if __name__ == "__main__":
+ default_matter_test_main()
diff --git a/src/python_testing/TC_WEBRTCP_2_13.py b/src/python_testing/TC_WEBRTCP_2_13.py
index f0ffa2c..3db4ed8 100644
--- a/src/python_testing/TC_WEBRTCP_2_13.py
+++ b/src/python_testing/TC_WEBRTCP_2_13.py
@@ -91,12 +91,12 @@
self.step(1)
# Allocate both Audio and Video streams
- audioStreamID = await self.allocate_one_audio_stream()
- videoStreamID = await self.allocate_one_video_stream()
+ audio_stream_id = await self.allocate_one_audio_stream()
+ video_stream_id = await self.allocate_one_video_stream()
# Validate that the streams were allocated successfully
- await self.validate_allocated_audio_stream(audioStreamID)
- await self.validate_allocated_video_stream(videoStreamID)
+ await self.validate_allocated_audio_stream(audio_stream_id)
+ await self.validate_allocated_video_stream(video_stream_id)
self.step(2)
# For CI: Use app pipe to simulate physical privacy switch being turned on
@@ -140,8 +140,8 @@
sdp=sdp_offer,
streamUsage=3,
originatingEndpointID=endpoint,
- videoStreamID=videoStreamID,
- audioStreamID=audioStreamID
+ videoStreamID=video_stream_id,
+ audioStreamID=audio_stream_id
)
try:
await self.send_single_cmd(cmd=cmd, endpoint=endpoint, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
diff --git a/src/python_testing/TC_WEBRTCP_2_16.py b/src/python_testing/TC_WEBRTCP_2_16.py
new file mode 100644
index 0000000..febde32
--- /dev/null
+++ b/src/python_testing/TC_WEBRTCP_2_16.py
@@ -0,0 +1,180 @@
+#
+# Copyright (c) 2025 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.
+#
+
+# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments
+# for details about the block below.
+#
+# === BEGIN CI TEST ARGUMENTS ===
+# test-runner-runs:
+# run1:
+# app: ${CAMERA_APP}
+# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# script-args: >
+# --PICS src/app/tests/suites/certification/ci-pics-values
+# --storage-path admin_storage.json
+# --commissioning-method on-network
+# --discriminator 1234
+# --passcode 20202021
+# --trace-to json:${TRACE_TEST_JSON}.json
+# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+# factory-reset: true
+# quiet: true
+# === END CI TEST ARGUMENTS ===
+#
+
+import logging
+
+from mobly import asserts
+from TC_WEBRTCPTestBase import WEBRTCPTestBase
+
+import matter.clusters as Clusters
+from matter import ChipDeviceCtrl
+from matter.clusters.Types import NullValue
+from matter.interaction_model import InteractionModelError, Status
+from matter.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+
+logger = logging.getLogger(__name__)
+
+
+class TC_WEBRTCP_2_16(MatterBaseTest, WEBRTCPTestBase):
+ def desc_TC_WEBRTCP_2_16(self) -> str:
+ """Returns a description of this test"""
+ return "[TC-WEBRTCP-2.16] Validate ProvideOffer resource exhaustion - PROVISIONAL"
+
+ def steps_TC_WEBRTCP_2_16(self) -> list[TestStep]:
+ steps = [
+ TestStep(1, "TH allocates both Audio and Video streams via AudioStreamAllocate and VideoStreamAllocate commands to CameraAVStreamManagement",
+ "DUT responds with success and provides stream IDs"),
+ TestStep(2, "TH sends multiple ProvideOffer commands to exhaust DUT's session capacity (DUT-specific limit)",
+ "DUT responds with ProvideOfferResponse for each until capacity is reached"),
+ TestStep(3, "TH sends an additional ProvideOffer command beyond DUT's capacity",
+ "DUT responds with RESOURCE_EXHAUSTED status code"),
+ ]
+ return steps
+
+ def pics_TC_WEBRTCP_2_16(self) -> list[str]:
+ pics = [
+ "WEBRTCP.S",
+ "AVSM.S",
+ ]
+ return pics
+
+ @async_test_body
+ async def test_TC_WEBRTCP_2_16(self):
+ """
+ Executes the test steps for validating ProvideOffer resource exhaustion.
+ """
+
+ endpoint = self.get_endpoint(default=1)
+
+ self.step(1)
+ # Allocate Audio and Video streams
+ audio_stream_id = await self.allocate_one_audio_stream()
+ video_stream_id = await self.allocate_one_video_stream()
+
+ # Validate that the streams were allocated successfully
+ await self.validate_allocated_audio_stream(audio_stream_id)
+ await self.validate_allocated_video_stream(video_stream_id)
+
+ self.step(2)
+ # Send multiple ProvideOffer commands to exhaust the DUT's capacity
+ logger.info("Starting to send ProvideOffer commands to exhaust DUT capacity")
+
+ # Get the maximum concurrent WebRTC sessions from user or use default for CI
+ prompt_msg = (
+ "\nPlease enter the maximum number of concurrent WebRTC sessions supported by the DUT:\n"
+ "This value is DUT-specific and should be obtained from the DUT documentation or specifications.\n"
+ "Enter the number (e.g., 5): "
+ )
+
+ if self.is_pics_sdk_ci_only:
+ # Use default value for CI testing
+ max_attempts = 5
+ logger.info(f"Using default max_attempts={max_attempts} for CI testing")
+ else:
+ # Prompt user for DUT-specific value
+ user_input = self.wait_for_user_input(prompt_msg)
+ try:
+ max_attempts = int(user_input.strip())
+ asserts.assert_true(max_attempts > 0, "Maximum concurrent sessions must be greater than 0")
+ logger.info(f"Using user-specified max_attempts={max_attempts}")
+ except ValueError:
+ asserts.fail(f"Invalid input '{user_input}'. Please enter a valid number.")
+
+ provide_offer_cmd = Clusters.WebRTCTransportProvider.Commands.ProvideOffer(
+ webRTCSessionID=NullValue,
+ sdp=(
+ "v=0\n"
+ "o=rtc 2281582238 0 IN IP4 127.0.0.1\n"
+ "s=-\n"
+ "t=0 0\n"
+ "a=group:BUNDLE 0\n"
+ "a=msid-semantic:WMS *\n"
+ "a=ice-options:ice2,trickle\n"
+ "a=fingerprint:sha-256 8F:BF:9A:B9:FA:59:EC:F6:08:EA:47:D3:F4:AC:FA:AC:E9:27:FA:28:D3:00:1D:9B:EF:62:3F:B8:C6:09:FB:B9\n"
+ "m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n"
+ "c=IN IP4 0.0.0.0\n"
+ "a=mid:0\n"
+ "a=sendrecv\n"
+ "a=sctp-port:5000\n"
+ "a=max-message-size:262144\n"
+ "a=setup:actpass\n"
+ "a=ice-ufrag:ytRw\n"
+ "a=ice-pwd:blrzPJtaV9Y1BNgbC1bXpi"
+ ),
+ streamUsage=3,
+ originatingEndpointID=1,
+ videoStreamID=video_stream_id,
+ audioStreamID=audio_stream_id
+ )
+
+ # Try to allocate multiple sessions to reach the DUT's capacity limit
+ for attempt in range(max_attempts):
+ logger.info(f"Attempt {attempt + 1}: Sending ProvideOffer command")
+ resp = await self.send_single_cmd(
+ cmd=provide_offer_cmd,
+ endpoint=1,
+ payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD
+ )
+ asserts.assert_equal(type(resp), Clusters.WebRTCTransportProvider.Commands.ProvideOfferResponse,
+ "Incorrect response type")
+ logger.info(f"Successfully created ProvideOffer session {attempt + 1}")
+
+ self.step(3)
+ # Send an additional ProvideOffer command when DUT capacity is exhausted
+ logger.info("Sending additional ProvideOffer command to trigger resource exhaustion")
+
+ # This should fail with RESOURCE_EXHAUSTED status
+ try:
+ resp = await self.send_single_cmd(
+ cmd=provide_offer_cmd,
+ endpoint=endpoint,
+ payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD
+ )
+ # If we reach here, the command succeeded when it should have failed
+ asserts.fail("Expected RESOURCE_EXHAUSTED error but ProvideOffer command succeeded")
+ except InteractionModelError as e:
+ # Verify that we got the expected RESOURCE_EXHAUSTED status
+ asserts.assert_equal(e.status, Status.ResourceExhausted,
+ f"Expected RESOURCE_EXHAUSTED status, got {e.status}")
+ logger.info(f"Correctly received RESOURCE_EXHAUSTED status: {e.status}")
+
+ logger.info("Successfully validated ProvideOffer resource exhaustion behavior")
+
+
+if __name__ == "__main__":
+ default_matter_test_main()