Skip to content

[Camera] Add CameraAppTestSuite for Matter Camera TCs #214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: v2.13-develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
CHIP_TOOL_EXE = "./chip-tool"
CHIP_TOOL_ARG_PAA_CERTS_PATH = "--paa-trust-store-path"

CHIP_CAMERA_CONTROLLER_EXE = "./chip-camera-controller"

# Chip App Parameters
CHIP_APP_EXE = "./chip-app1"

Expand All @@ -49,6 +51,7 @@ class UnsupportedChipServerType(Exception):
class ChipServerType(str, Enum):
CHIP_TOOL = "chip-tool"
CHIP_APP = "chip-app"
CHIP_CAMERA_CONTROLLER = "chip-camera-controller"


class ChipServer(metaclass=Singleton):
Expand Down Expand Up @@ -119,7 +122,10 @@ async def start(
self.__use_paa_certs = use_paa_certs
self.__server_type = server_type

if server_type == ChipServerType.CHIP_TOOL:
if server_type == ChipServerType.CHIP_CAMERA_CONTROLLER:
prefix = CHIP_CAMERA_CONTROLLER_EXE
command = ["interactive", "server"]
elif server_type == ChipServerType.CHIP_TOOL:
prefix = CHIP_TOOL_EXE
command = ["interactive", "server"]
elif server_type == ChipServerType.CHIP_APP:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ def test_test_type_all_disabled_steps() -> None:
disabled_step = MatterTestStep(label="Disabled Test Step", disabled=True)
five_disabled_steps_test = yaml_test_instance(tests=[disabled_step] * 5)

type = _test_type(five_disabled_steps_test)
type, _ = _test_type(five_disabled_steps_test)
assert type == MatterTestType.MANUAL

# simulated in path overrides test type to simulated
five_disabled_steps_test.path = Path("TC_XX_Simulated.yaml")
type = _test_type(five_disabled_steps_test)
type, _ = _test_type(five_disabled_steps_test)
assert type == MatterTestType.SIMULATED


Expand All @@ -117,25 +117,25 @@ def test_test_type_some_disabled_steps() -> None:
enabled_step = MatterTestStep(label="Enabled Test Step", disabled=False)
test = yaml_test_instance(tests=[disabled_step, enabled_step])

type = _test_type(test)
type, _ = _test_type(test)
assert type == MatterTestType.AUTOMATED

# simulated in path overrides test type to simulated
test.path = Path("TC_XX_Simulated.yaml")
type = _test_type(test)
type, _ = _test_type(test)
assert type == MatterTestType.SIMULATED


def test_test_type_all_enabled_steps_no_prompts() -> None:
enabled_step = MatterTestStep(label="Enabled Test Step")
five_enabled_steps_test = yaml_test_instance(tests=[enabled_step] * 5)

type = _test_type(five_enabled_steps_test)
type, _ = _test_type(five_enabled_steps_test)
assert type == MatterTestType.AUTOMATED

# simulated in path overrides test type to simulated
five_enabled_steps_test.path = Path("TC_XX_Simulated.yaml")
type = _test_type(five_enabled_steps_test)
type, _ = _test_type(five_enabled_steps_test)
assert type == MatterTestType.SIMULATED


Expand All @@ -144,10 +144,10 @@ def test_test_type_all_enabled_steps_some_prompts() -> None:
prompt_step = MatterTestStep(label="Prompt Test Step", command="UserPrompt")
test = yaml_test_instance(tests=[enabled_step, prompt_step])

type = _test_type(test)
type, _ = _test_type(test)
assert type == MatterTestType.SEMI_AUTOMATED

# simulated in path overrides test type to simulated
test.path = Path("TC_XX_Simulated.yaml")
type = _test_type(test)
type, _ = _test_type(test)
assert type == MatterTestType.SIMULATED
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,11 @@ async def run_test(
[test_path], parser_config, test_parser_hooks
)

if server_type == ChipServerType.CHIP_TOOL:
# Reuse chip-tool adapter for camera-controller
if (
server_type == ChipServerType.CHIP_TOOL
or server_type == ChipServerType.CHIP_CAMERA_CONTROLLER
):
adapter = ChipToolAdapter.Adapter(parser_config.definitions)
elif server_type == ChipServerType.CHIP_APP:
adapter = ChipAppAdapter.Adapter(parser_config.definitions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ async def setup(self) -> None:
self.runner.reset_pics_state()

self.__dut_commissioned_successfully = False
if self.server_type == ChipServerType.CHIP_TOOL:
if (
self.server_type == ChipServerType.CHIP_TOOL
or self.server_type == ChipServerType.CHIP_CAMERA_CONTROLLER
):
logger.info("Commission DUT")
user_response = await prompt_for_commissioning_mode(
self, logger, None, self.cancel
Expand Down Expand Up @@ -227,7 +230,10 @@ async def cleanup(self) -> None:
# Only unpair if commissioning was successfull during setup
if self.__dut_commissioned_successfully:
# Unpair is not applicable for simulated apps case
if self.server_type == ChipServerType.CHIP_TOOL:
if (
self.server_type == ChipServerType.CHIP_TOOL
or self.server_type == ChipServerType.CHIP_CAMERA_CONTROLLER
):
logger.info("Unpairing DUT from server")
await self.runner.unpair()
elif self.server_type == ChipServerType.CHIP_APP:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright (c) 2025 Project CHIP Authors
#
# 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.
#

USER_ACTIONS = ["UserPrompt", "PromptWithResponse", "VerifyVideoStream"]
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from ...chip.chip_server import ChipServerType
from ...models.matter_test_models import MatterTestStep, MatterTestType
from ...yaml_tests.models.chip_test import ChipManualPromptTest, ChipTest
from .constants.yaml_constants import USER_ACTIONS
from .test_suite import SuiteType
from .yaml_test_models import YamlTest

# Custom type variable used to annotate the factory method in YamlTestCase.
Expand Down Expand Up @@ -81,11 +83,18 @@ def class_factory(cls, test: YamlTest, yaml_version: str) -> Type[T]:
if test.type == MatterTestType.MANUAL:
case_class = YamlManualTestCase
elif test.type == MatterTestType.SEMI_AUTOMATED:
case_class = YamlSemiAutomatedChipTestCase
if test.suite_type == SuiteType.CAMERA_AUTOMATED:
case_class = YamlCameraSemiAutomatedChipTestCase
else:
case_class = YamlSemiAutomatedChipTestCase

elif test.type == MatterTestType.SIMULATED:
case_class = YamlSimulatedTestCase
else: # Automated
case_class = YamlChipTestCase
if test.suite_type == SuiteType.CAMERA_AUTOMATED:
case_class = YamlCameraChipTestCase
else:
case_class = YamlChipTestCase

return case_class.__class_factory(test=test, yaml_version=yaml_version)

Expand Down Expand Up @@ -165,7 +174,8 @@ def _append_automated_test_step(self, yaml_step: MatterTestStep) -> None:
Disabled steps are ignored.
(Such tests will be marked as 'Steps Disabled' elsewhere)

UserPrompt are special cases that will prompt test operator for input.
UserPrompt, PromptWithResponse or VerifyVideoStream are special cases that will
prompt test operator for input.
"""
if yaml_step.disabled:
test_engine_logger.info(
Expand All @@ -174,7 +184,7 @@ def _append_automated_test_step(self, yaml_step: MatterTestStep) -> None:
return

step = TestStep(yaml_step.label)
if yaml_step.command == "UserPrompt":
if yaml_step.command in USER_ACTIONS:
step = ManualVerificationTestStep(
name=yaml_step.label,
verification=yaml_step.verification,
Expand Down Expand Up @@ -208,6 +218,23 @@ def create_test_steps(self) -> None:
self._append_automated_test_step(step)


class YamlCameraChipTestCase(YamlTestCase, ChipTest):
"""Automated test cases using chip-camera-controller."""

server_type = ChipServerType.CHIP_CAMERA_CONTROLLER

def create_test_steps(self) -> None:
self.test_steps = [TestStep("Start chip-camera-controller test")]
for step in self.yaml_test.steps:
self._append_automated_test_step(step)


class YamlCameraSemiAutomatedChipTestCase(YamlCameraChipTestCase, ChipManualPromptTest):
"""Camera Semi-Automated test cases, need special step for users to attach logs
for manual steps, so inheriting from ChipManualPromptTest.
"""


class YamlSemiAutomatedChipTestCase(YamlChipTestCase, ChipManualPromptTest):
"""Semi-Automated test cases, need special step for users to attach logs
for manual steps, so inheriting from ChipManualPromptTest.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from typing import Type
from typing import Optional, Type

from app.test_engine.models.test_declarations import (
TestCaseDeclaration,
Expand Down Expand Up @@ -53,11 +53,13 @@ class YamlCaseDeclaration(TestCaseDeclaration):
"""Direct initialization for YAML Test Case."""

class_ref: Type[YamlTestCase]
suite_type: Optional[SuiteType] = None

def __init__(self, test: YamlTest, yaml_version: str) -> None:
super().__init__(
YamlTestCase.class_factory(test=test, yaml_version=yaml_version)
)
self.suite_type = test.suite_type

@property
def test_type(self) -> MatterTestType:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class SuiteType(Enum):
SIMULATED = 1
AUTOMATED = 2
MANUAL = 3
CAMERA_AUTOMATED = 4


# Custom Type variable used to annotate the factory methods of classmethod.
Expand Down Expand Up @@ -69,6 +70,8 @@ def class_factory(
suite_class = ManualYamlTestSuite
elif suite_type == SuiteType.SIMULATED:
suite_class = SimulatedYamlTestSuite
elif suite_type == SuiteType.CAMERA_AUTOMATED:
suite_class = ChipCameraYamlTestSuite
elif suite_type == SuiteType.AUTOMATED:
suite_class = ChipYamlTestSuite

Expand Down Expand Up @@ -112,5 +115,15 @@ async def setup(self) -> None:
await ChipSuite.setup(self)


class ChipCameraYamlTestSuite(YamlTestSuite, ChipSuite):
server_type = ChipServerType.CHIP_CAMERA_CONTROLLER

async def setup(self) -> None:
"""Due top multi inheritance, we need to call setup on both super classes."""
self.server_type = ChipServerType.CHIP_CAMERA_CONTROLLER
await YamlTestSuite.setup(self)
await ChipSuite.setup(self)


class SimulatedYamlTestSuite(ChipYamlTestSuite):
server_type = ChipServerType.CHIP_APP
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from typing import Any
from typing import Any, Optional

from pydantic_yaml import YamlModelMixin

from ...models.matter_test_models import MatterTest
from .test_suite import SuiteType

###
# This file declares YAML models that are used to parse the YAML Test Cases.
###


class YamlTest(YamlModelMixin, MatterTest):
suite_type: Optional[SuiteType] = None

def __init__(self, **kwargs: Any) -> None:
super().__init__(steps=kwargs["tests"], **kwargs)
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,68 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
import re
from pathlib import Path

from loguru import logger
from pydantic import ValidationError

from ...models.matter_test_models import MatterTestType
from .constants.yaml_constants import USER_ACTIONS
from .test_suite import SuiteType
from .yaml_test_models import YamlTest


class YamlParserException(Exception):
"""Raised when an error occurs during the parser of yaml file."""


def _test_type(test: YamlTest) -> MatterTestType:
"""Determine the type of a test based on the parsed yaml.
def _test_type(test: YamlTest) -> tuple[MatterTestType, SuiteType]:
"""Determine the type of a test and test suite based on the parsed yaml.

This is mainly determined by the number of disabled test steps.

Args:
test (YamlTest): parsed yaml model

Returns:
TestType:
- Manual: All steps disabled
- Semi-Automated: some steps are disabled
- Automated: no disabled steps
- Simulated: Tests where file name have "Simulated"
tuple[MatterTestType, SuiteType]:
SuiteType:
- Manual: All steps disabled
- Semi-Automated: some steps are disabled
- Automated: no disabled steps
- Simulated: Tests where file name have "Simulated"
SuiteType:
- SIMULATED: Simulated Test Suite
- AUTOMATED: Automated Test Suite
- MANUAL: Manual Test Suite
- CAMERA_AUTOMATED: Automated Camera Test Suite
"""
camera_test_pattern = r"\[TC-WEBRTC-\d+\.\d+\]"

if test.path is not None and "Simulated" in str(test.path):
return MatterTestType.SIMULATED
return MatterTestType.SIMULATED, SuiteType.SIMULATED

steps = test.steps

# If all disabled:
if all(s.disabled is True for s in steps):
return MatterTestType.MANUAL
return MatterTestType.MANUAL, SuiteType.MANUAL

# if any step has a UserPrompt, categorize as semi-automated
if any(s.command == "UserPrompt" for s in steps):
return MatterTestType.SEMI_AUTOMATED
# if any step has a UserPrompt, PromptWithResponse or VerifyVideoStream command,
# categorize as semi-automated
if any(s.command in USER_ACTIONS for s in steps):
if re.search(camera_test_pattern, test.name):
return MatterTestType.SEMI_AUTOMATED, SuiteType.CAMERA_AUTOMATED
else:
return MatterTestType.SEMI_AUTOMATED, SuiteType.AUTOMATED

# Otherwise Automated
return MatterTestType.AUTOMATED
# Otherwise
# If test case is camera related, then return SuiteType.CAMERA_AUTOMATED
if re.search(camera_test_pattern, test.name):
return MatterTestType.AUTOMATED, SuiteType.CAMERA_AUTOMATED
else:
return MatterTestType.AUTOMATED, SuiteType.AUTOMATED


def parse_yaml_test(path: Path) -> YamlTest:
Expand All @@ -68,7 +87,9 @@ def parse_yaml_test(path: Path) -> YamlTest:
yaml_str = file.read()
test = YamlTest.parse_raw(yaml_str, proto="yaml")
test.path = path
test.type = _test_type(test)
test_type, suite_type = _test_type(test)
test.type = test_type
test.suite_type = suite_type
except ValidationError as e:
logger.error(str(e))
raise YamlParserException(f"The YAML file {path} is invalid") from e
Expand Down
Loading