| # |
| # 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()) |