blob: 1b8b1a939d138f985874b3828b36151cad3da548 [file]
#
# Copyright (c) 2026 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.
#
"""Unit tests for MatterBaseTest metadata extraction"""
import sys
from mobly import signals
from matter.testing.decorators import async_test_body, pics
from matter.testing.matter_test_config import MatterTestConfig
from matter.testing.matter_testing import MatterBaseTest
from matter.testing.runner import TestStep, generate_mobly_test_config
def _make_test_instance(test_class):
config = generate_mobly_test_config(MatterTestConfig())
return test_class(config)
def test_pics_decorator_only():
"""@pics decorator is used when no pics_* method exists."""
class MyTest(MatterBaseTest):
@pics("OO.S", "S.S")
@async_test_body
async def test_TC_FOO_1_1(self):
pass
inst = _make_test_instance(MyTest)
result = inst.get_test_pics("test_TC_FOO_1_1")
assert result == ["OO.S", "S.S"], f"Expected ['OO.S', 'S.S'], got {result}"
def test_pics_method_takes_precedence():
"""Explicit pics_* method takes precedence over @pics decorator."""
class MyTest(MatterBaseTest):
def pics_TC_FOO_1_1(self):
return ["FROM.METHOD"]
@pics("FROM.DECORATOR")
@async_test_body
async def test_TC_FOO_1_1(self):
pass
inst = _make_test_instance(MyTest)
result = inst.get_test_pics("test_TC_FOO_1_1")
assert result == ["FROM.METHOD"], f"Expected ['FROM.METHOD'], got {result}"
def test_no_pics_at_all():
"""No pics_* method and no @pics decorator returns empty list."""
class MyTest(MatterBaseTest):
@async_test_body
async def test_TC_FOO_1_1(self):
pass
inst = _make_test_instance(MyTest)
result = inst.get_test_pics("test_TC_FOO_1_1")
assert result == [], f"Expected [], got {result}"
def test_pics_decorator_below_async_test_body():
"""@pics works when placed below @async_test_body (wraps propagates __dict__)."""
class MyTest(MatterBaseTest):
@async_test_body
@pics("OO.S")
async def test_TC_FOO_1_1(self):
pass
inst = _make_test_instance(MyTest)
result = inst.get_test_pics("test_TC_FOO_1_1")
assert result == ["OO.S"], f"Expected ['OO.S'], got {result}"
def test_pics_decorator_single_code():
"""@pics with a single PICS code."""
class MyTest(MatterBaseTest):
@pics("GRPKEY.S")
@async_test_body
async def test_TC_FOO_1_1(self):
pass
inst = _make_test_instance(MyTest)
result = inst.get_test_pics("test_TC_FOO_1_1")
assert result == ["GRPKEY.S"], f"Expected ['GRPKEY.S'], got {result}"
def test_pics_empty_raises():
"""@pics() with no arguments raises ValueError."""
try:
@pics()
def test_foo(self):
pass
assert False, "Expected ValueError for empty @pics"
except ValueError as e:
assert "at least one" in str(e).lower()
def test_pics_double_application_raises():
"""Applying @pics twice raises ValueError."""
try:
@pics("A.S")
@pics("B.S")
def test_foo(self):
pass
assert False, "Expected ValueError for double @pics"
except ValueError as e:
assert "more than once" in str(e).lower()
def test_desc_from_docstring():
"""Docstring is used as description when no desc_* method exists."""
class MyTest(MatterBaseTest):
@async_test_body
async def test_TC_FOO_1_1(self):
"""4.2.4. [TC-FOO-1.1] Scenes Management Cluster Interaction"""
pass
inst = _make_test_instance(MyTest)
result = inst.get_test_desc("test_TC_FOO_1_1")
assert result == "4.2.4. [TC-FOO-1.1] Scenes Management Cluster Interaction", f"Unexpected desc: {result}"
def test_desc_method_takes_precedence():
"""Explicit desc_* method takes precedence over docstring."""
class MyTest(MatterBaseTest):
def desc_TC_FOO_1_1(self):
return "From method"
@async_test_body
async def test_TC_FOO_1_1(self):
"""From docstring"""
pass
inst = _make_test_instance(MyTest)
result = inst.get_test_desc("test_TC_FOO_1_1")
assert result == "From method", f"Unexpected desc: {result}"
def test_desc_falls_back_to_method_name():
"""No desc_* and no docstring falls back to method name."""
class MyTest(MatterBaseTest):
@async_test_body
async def test_TC_FOO_1_1(self):
pass
inst = _make_test_instance(MyTest)
result = inst.get_test_desc("test_TC_FOO_1_1")
assert result == "test_TC_FOO_1_1", f"Unexpected desc: {result}"
def test_steps_from_explicit_method():
"""Explicit steps_* method is used when defined."""
class MyTest(MatterBaseTest):
def steps_TC_FOO_1_1(self):
return [TestStep(1, "Explicit step")]
@async_test_body
async def test_TC_FOO_1_1(self):
self.step(1, "Inline step")
inst = _make_test_instance(MyTest)
steps = inst.get_defined_test_steps("test_TC_FOO_1_1")
assert len(steps) == 1
assert steps[0].description == "Explicit step", f"Expected explicit method to win, got: {steps[0].description}"
def test_steps_from_ast_fallback():
"""AST extraction is used when no steps_* method exists."""
class MyTest(MatterBaseTest):
@async_test_body
async def test_TC_FOO_1_1(self):
self.step(1, "First inline step")
self.step(2, "Second inline step")
inst = _make_test_instance(MyTest)
steps = inst.get_defined_test_steps("test_TC_FOO_1_1")
assert steps is not None, "Expected AST fallback to find steps"
assert len(steps) == 2
assert steps[0].test_plan_number == 1
assert steps[0].description == "First inline step"
assert steps[1].test_plan_number == 2
assert steps[1].description == "Second inline step"
def test_steps_inline_runtime_validation():
"""Inline step descriptions work end-to-end with step validation."""
from types import SimpleNamespace
class MyTest(MatterBaseTest):
@async_test_body
async def test_TC_FOO_1_1(self):
self.step(1, "First step")
self.step(2, "Second step")
inst = _make_test_instance(MyTest)
# Manually set up the per-test state that Mobly normally initializes
# before each test method. We can't call setup_test() because it
# requires the full Mobly test runner lifecycle and chip stack.
inst.current_test_info = SimpleNamespace(name="test_TC_FOO_1_1")
inst.current_step_index = 0
inst.step_skipped = False
inst.step_start_time = None
# These should succeed — steps match the AST-extracted plan
inst.step(1, "First step")
inst.step(2, "Second step")
# Calling a step out of order should fail
inst.current_step_index = 0
try:
inst.step(2, "Wrong order")
assert False, "Expected TestFailure for out-of-order step"
except signals.TestFailure:
pass
def test_steps_none_when_no_steps():
"""No steps_* and no self.step() calls returns None."""
class MyTest(MatterBaseTest):
@async_test_body
async def test_TC_FOO_1_1(self):
pass
inst = _make_test_instance(MyTest)
steps = inst.get_defined_test_steps("test_TC_FOO_1_1")
assert steps is None, f"Expected None, got: {steps}"
def main():
failures = []
test_functions = [
test_pics_decorator_only,
test_pics_method_takes_precedence,
test_no_pics_at_all,
test_pics_decorator_below_async_test_body,
test_pics_decorator_single_code,
test_pics_empty_raises,
test_pics_double_application_raises,
test_desc_from_docstring,
test_desc_method_takes_precedence,
test_desc_falls_back_to_method_name,
test_steps_from_explicit_method,
test_steps_from_ast_fallback,
test_steps_inline_runtime_validation,
test_steps_none_when_no_steps,
]
for test_fn in test_functions:
try:
test_fn()
print(f" PASS: {test_fn.__name__}")
except Exception as e:
print(f" FAIL: {test_fn.__name__}: {e}")
failures.append(test_fn.__name__)
print(f"\nTestTestMetadata: {len(failures)} failures out of {len(test_functions)} tests")
for f in failures:
print(f" FAILED: {f}")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())