Skip to content

Commit 06caebe

Browse files
Python scripting: Add a test plan generator script (#32718)
* Python scriptiong: Add a test plan generator script * Fix linter * Restyled by isort * fix spacing * update spacing on expected outcome * remove init file - it snuck in, but isn't needed * Add documnetation and better error reporting. * make print a little nicer for help * linter... * Restyled by isort * make defined steps function public --------- Co-authored-by: Restyled.io <commits@restyled.io>
1 parent 6722693 commit 06caebe

File tree

2 files changed

+99
-4
lines changed

2 files changed

+99
-4
lines changed

src/python_testing/matter_testing_support.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -684,10 +684,10 @@ def get_test_steps(self, test: str) -> list[TestStep]:
684684
in order using self.step(number), where number is the test_plan_number
685685
from each TestStep.
686686
'''
687-
steps = self._get_defined_test_steps(test)
687+
steps = self.get_defined_test_steps(test)
688688
return [TestStep(1, "Run entire test")] if steps is None else steps
689689

690-
def _get_defined_test_steps(self, test: str) -> list[TestStep]:
690+
def get_defined_test_steps(self, test: str) -> list[TestStep]:
691691
steps_name = 'steps_' + test[5:]
692692
try:
693693
fn = getattr(self, steps_name)
@@ -781,7 +781,7 @@ def setup_test(self):
781781
self.step_skipped = False
782782
if self.runner_hook and not self.is_commissioning:
783783
test_name = self.current_test_info.name
784-
steps = self._get_defined_test_steps(test_name)
784+
steps = self.get_defined_test_steps(test_name)
785785
num_steps = 1 if steps is None else len(steps)
786786
filename = inspect.getfile(self.__class__)
787787
desc = self.get_test_desc(test_name)
@@ -977,7 +977,7 @@ def on_pass(self, record):
977977
self.runner_hook.step_success(logger=None, logs=None, duration=step_duration, request=None)
978978

979979
# TODO: this check could easily be annoying when doing dev. flag it somehow? Ditto with the in-order check
980-
steps = self._get_defined_test_steps(record.test_name)
980+
steps = self.get_defined_test_steps(record.test_name)
981981
if steps is None:
982982
# if we don't have a list of steps, assume they were all run
983983
all_steps_run = True
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env -S python3 -B
2+
#
3+
# Copyright (c) 2024 Project CHIP Authors
4+
# All rights reserved.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
import importlib
19+
import logging
20+
import os
21+
import sys
22+
from pathlib import Path
23+
24+
import click
25+
from matter_testing_support import MatterTestConfig, generate_mobly_test_config
26+
27+
28+
def indent_multiline(multiline: str, num_spaces: int) -> str:
29+
''' Indents subsequent lines of a multiline string by num_spaces spaces'''
30+
s = multiline.split('\n')
31+
s = [(num_spaces * ' ' + line.lstrip()).rstrip() for line in s]
32+
return '\n'.join(s).lstrip()
33+
34+
35+
@click.command()
36+
@click.argument('filename', type=click.Path(exists=True))
37+
@click.argument('classname', type=str)
38+
@click.argument('test', type=str)
39+
def main(filename, classname, test):
40+
'''
41+
This script generates the Test Procedure table for the test plans document
42+
from the python script steps. In order to use this generator, the test
43+
automation script conform to the following requirements:
44+
45+
- automated in python\n
46+
- test implements the steps_ function to provide steps information to the TH\n
47+
- TestStep list returned from the steps_ function includes both description and expectation fields\n
48+
- test does not gate any steps on PICS (top level PICS ok)\n
49+
50+
51+
Usage: test_plan_table_generator.py filename classname test
52+
53+
filename - name of the file where the test is automated\n
54+
classname - name of the MatterBaseTest class\n
55+
test - name of the test to generate the table for (include the test_ portion)\n
56+
'''
57+
try:
58+
module = importlib.import_module(Path(os.path.basename(filename)).stem)
59+
except ModuleNotFoundError:
60+
logging.error(f'Unable to load python module from {filename}. Please ensure this is a valid python file path')
61+
return -1
62+
63+
try:
64+
test_class = getattr(module, classname)
65+
except AttributeError:
66+
logging.error(f'Unable to load the test class {classname}. Please ensure this class is implemented in {filename}')
67+
return -1
68+
69+
config = generate_mobly_test_config(MatterTestConfig())
70+
test_instance = test_class(config)
71+
steps = test_instance.get_defined_test_steps(test)
72+
if not steps:
73+
logging.error(f'Unable to find steps for test {test}. Please ensure the steps_ function is implemented')
74+
return -1
75+
76+
indent = 6
77+
header_num = f'{"**#**":<{indent}}'
78+
header_num_step = f'|{header_num} |*TestStep* '
79+
s = ('[cols="5%,45%,45%"]\n'
80+
'|===\n'
81+
f'{header_num_step}|*Expected Outcome*\n')
82+
for step in steps:
83+
step_num = f'|{step.test_plan_number:<{indent}}a|'
84+
s += f'{step_num}{indent_multiline(step.description, len(step_num))}\n'
85+
86+
padding = (len(header_num_step) - 1) * ' '
87+
# add 2 to indent for a| at start
88+
s += f'{padding}a|{indent_multiline(step.expectation, len(padding)+2)}\n\n'
89+
s += '|===\n'
90+
91+
print(s)
92+
93+
94+
if __name__ == "__main__":
95+
sys.exit(main())

0 commit comments

Comments
 (0)