Define CI tests arguments in YAML format (#35770)
* Define CI tests arguments in YAML format
* Convert ACE and ACL tests arguments to YAML
* Update doc for CI test arguments
* Restyled by prettier-markdown
* Improve error reporting
---------
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/docs/testing/python.md b/docs/testing/python.md
index c631550..9c2edcf 100644
--- a/docs/testing/python.md
+++ b/docs/testing/python.md
@@ -644,74 +644,70 @@
Example to compile all prerequisites and then running all python tests:
-```
+```shell
./scripts/tests/local.py build # will compile python in out/pyenv and ALL application prerequisites
./scripts/tests/local.py python-tests # Runs all python tests that are runnable in CI
```
## Defining the CI test arguments
-Below is the format of the structured environment definition comments:
+Arguments required to run a test can be defined in the comment block at the top
+of the test script. The section with the arguments should be placed between the
+`# === BEGIN CI TEST ARGUMENTS ===` and `# === END CI TEST ARGUMENTS ===`
+markers. Arguments should be structured as a valid YAML dictionary with a root
+key `test-runner-runs`, followed by the run identifier, and then the parameters
+for that run, e.g.:
-```
+```python
# 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: <run_identifier>
-# test-runner-run/<run_identifier>/app: ${TYPE_OF_APP}
-# test-runner-run/<run_identifier>/factoryreset: <True|False>
-# test-runner-run/<run_identifier>/quiet: <True|False>
-# test-runner-run/<run_identifier>/app-args: <app_arguments>
-# test-runner-run/<run_identifier>/script-args: <script_arguments>
+# test-runner-runs:
+# run1:
+# app: ${TYPE_OF_APP}
+# factoryreset: <true|false>
+# quiet: <true|false>
+# app-args: <app_arguments>
+# script-args: <script_arguments>
# === END CI TEST ARGUMENTS ===
```
-NOTE: The `=== BEGIN CI TEST ARGUMENTS ===` and `=== END CI TEST ARGUMENTS ===`
-markers must be present.
-
### Description of Parameters
-- `test-runner-runs`: Specifies the identifier for the run. This can be any
- unique identifier.
-
- - Example: `run1`
-
-- `test-runner-run/<run_identifier>/app`: Indicates the application to be used
- in the test. Different app types as needed could be referenced from section
- [name: Generate an argument environment file ] of the file
+- `app`: Indicates the application to be used in the test. Different app types
+ as needed could be referenced from section [name: Generate an argument
+ environment file ] of the file
[.github/workflows/tests.yaml](https://github.com/project-chip/connectedhomeip/blob/master/.github/workflows/tests.yaml)
- - Example: `${TYPE_OF_APP}`
+ - Example: `${TYPE_OF_APP}`
-- `test-runner-run/<run_identifier>/factoryreset`: Determines whether a
- factory reset should be performed before the test.
+- `factoryreset`: Determines whether a factory reset should be performed
+ before the test.
- - Example: `True`
+ - Example: `true`
-- `test-runner-run/<run_identifier>/quiet`: Sets the verbosity level of the
- test run. When set to True, the test run will be quieter.
+- `quiet`: Sets the verbosity level of the test run. When set to True, the
+ test run will be quieter.
- - Example: `True`
+ - Example: `true`
-- `test-runner-run/<run_identifier>/app-args`: Specifies the arguments to be
- passed to the application during the test.
+- `app-args`: Specifies the arguments to be passed to the application during
+ the test.
- Example:
`--discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json`
-- `test-runner-run/<run_identifier>/script-args`: Specifies the arguments to
- be passed to the test script.
+- `script-args`: Specifies the arguments to be passed to the test script.
- Example:
`--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`
-- `test-runner-run/<run_identifier>/script-start-delay`: Specifies the number
- of seconds to wait before starting the test script. This parameter can be
- used to allow the application to initialize itself properly before the test
- script will try to commission it (e.g. in case if the application needs to
- be commissioned to some other controller first). By default, the delay is 0
- seconds.
+- `script-start-delay`: Specifies the number of seconds to wait before
+ starting the test script. This parameter can be used to allow the
+ application to initialize itself properly before the test script will try to
+ commission it (e.g. in case if the application needs to be commissioned to
+ some other controller first). By default, the delay is 0 seconds.
- Example: `10`
diff --git a/src/python_testing/TC_ACE_1_2.py b/src/python_testing/TC_ACE_1_2.py
index 9bd2652..7d227f5 100644
--- a/src/python_testing/TC_ACE_1_2.py
+++ b/src/python_testing/TC_ACE_1_2.py
@@ -19,12 +19,19 @@
# for details about the block below.
#
# === BEGIN CI TEST ARGUMENTS ===
-# test-runner-runs: run1
-# test-runner-run/run1/app: ${ALL_CLUSTERS_APP}
-# test-runner-run/run1/factoryreset: True
-# test-runner-run/run1/quiet: True
-# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
-# test-runner-run/run1/script-args: --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
+# test-runner-runs:
+# run1:
+# app: ${ALL_CLUSTERS_APP}
+# factoryreset: true
+# quiet: true
+# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# script-args: >
+# --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
# === END CI TEST ARGUMENTS ===
import logging
diff --git a/src/python_testing/TC_ACE_1_3.py b/src/python_testing/TC_ACE_1_3.py
index 3c64171..29aabf3 100644
--- a/src/python_testing/TC_ACE_1_3.py
+++ b/src/python_testing/TC_ACE_1_3.py
@@ -19,12 +19,19 @@
# for details about the block below.
#
# === BEGIN CI TEST ARGUMENTS ===
-# test-runner-runs: run1
-# test-runner-run/run1/app: ${ALL_CLUSTERS_APP}
-# test-runner-run/run1/factoryreset: True
-# test-runner-run/run1/quiet: True
-# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
-# test-runner-run/run1/script-args: --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
+# test-runner-runs:
+# run1:
+# app: ${ALL_CLUSTERS_APP}
+# factoryreset: true
+# quiet: true
+# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# script-args: >
+# --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
# === END CI TEST ARGUMENTS ===
import logging
diff --git a/src/python_testing/TC_ACE_1_4.py b/src/python_testing/TC_ACE_1_4.py
index f6f04e3..9344b0b 100644
--- a/src/python_testing/TC_ACE_1_4.py
+++ b/src/python_testing/TC_ACE_1_4.py
@@ -19,12 +19,21 @@
# for details about the block below.
#
# === BEGIN CI TEST ARGUMENTS ===
-# test-runner-runs: run1
-# test-runner-run/run1/app: ${ALL_CLUSTERS_APP}
-# test-runner-run/run1/factoryreset: True
-# test-runner-run/run1/quiet: True
-# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
-# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --int-arg PIXIT.ACE.APPENDPOINT:1 PIXIT.ACE.APPDEVTYPEID:0x0100 --string-arg PIXIT.ACE.APPCLUSTER:OnOff PIXIT.ACE.APPATTRIBUTE:OnOff --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+# test-runner-runs:
+# run1:
+# app: ${ALL_CLUSTERS_APP}
+# factoryreset: true
+# quiet: true
+# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# script-args: >
+# --storage-path admin_storage.json
+# --commissioning-method on-network
+# --discriminator 1234
+# --passcode 20202021
+# --int-arg PIXIT.ACE.APPENDPOINT:1 PIXIT.ACE.APPDEVTYPEID:0x0100
+# --string-arg PIXIT.ACE.APPCLUSTER:OnOff PIXIT.ACE.APPATTRIBUTE:OnOff
+# --trace-to json:${TRACE_TEST_JSON}.json
+# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
# === END CI TEST ARGUMENTS ===
import sys
diff --git a/src/python_testing/TC_ACE_1_5.py b/src/python_testing/TC_ACE_1_5.py
index 00bd343..cda4187 100644
--- a/src/python_testing/TC_ACE_1_5.py
+++ b/src/python_testing/TC_ACE_1_5.py
@@ -19,12 +19,20 @@
# for details about the block below.
#
# === BEGIN CI TEST ARGUMENTS ===
-# test-runner-runs: run1
-# test-runner-run/run1/app: ${ALL_CLUSTERS_APP}
-# test-runner-run/run1/factoryreset: True
-# test-runner-run/run1/quiet: True
-# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
-# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+# test-runner-runs:
+# run1:
+# app: ${ALL_CLUSTERS_APP}
+# factoryreset: true
+# quiet: true
+# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# script-args: >
+# --storage-path admin_storage.json
+# --commissioning-method on-network
+# --discriminator 1234
+# --passcode 20202021
+# --PICS src/app/tests/suites/certification/ci-pics-values
+# --trace-to json:${TRACE_TEST_JSON}.json
+# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
# === END CI TEST ARGUMENTS ===
import logging
diff --git a/src/python_testing/TC_ACL_2_11.py b/src/python_testing/TC_ACL_2_11.py
index f5ebed2..88ef1d2 100644
--- a/src/python_testing/TC_ACL_2_11.py
+++ b/src/python_testing/TC_ACL_2_11.py
@@ -19,12 +19,23 @@
# for details about the block below.
#
# === BEGIN CI TEST ARGUMENTS ===
-# test-runner-runs: run1
-# test-runner-run/run1/app: ${NETWORK_MANAGEMENT_APP}
-# test-runner-run/run1/factoryreset: True
-# test-runner-run/run1/quiet: True
-# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json --commissioning-arl-entries "[{\"endpoint\": 1,\"cluster\": 1105,\"restrictions\": [{\"type\": 0,\"id\": 0}]}]" --arl-entries "[{\"endpoint\": 1,\"cluster\": 1105,\"restrictions\": [{\"type\": 0,\"id\": 0}]}]"
-# test-runner-run/run1/script-args: --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
+# test-runner-runs:
+# run1:
+# app: ${NETWORK_MANAGEMENT_APP}
+# factoryreset: true
+# quiet: true
+# app-args: >
+# --discriminator 1234 --KVS kvs1
+# --trace-to json:${TRACE_APP}.json
+# --commissioning-arl-entries "[{\"endpoint\": 1,\"cluster\": 1105,\"restrictions\": [{\"type\": 0,\"id\": 0}]}]"
+# --arl-entries "[{\"endpoint\": 1,\"cluster\": 1105,\"restrictions\": [{\"type\": 0,\"id\": 0}]}]"
+# script-args: >
+# --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
# === END CI TEST ARGUMENTS ===
import logging
diff --git a/src/python_testing/TC_ACL_2_2.py b/src/python_testing/TC_ACL_2_2.py
index a1e755b..b0880b0 100644
--- a/src/python_testing/TC_ACL_2_2.py
+++ b/src/python_testing/TC_ACL_2_2.py
@@ -16,12 +16,19 @@
#
# === BEGIN CI TEST ARGUMENTS ===
-# test-runner-runs: run1
-# test-runner-run/run1/app: ${ALL_CLUSTERS_APP}
-# test-runner-run/run1/factoryreset: True
-# test-runner-run/run1/quiet: True
-# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
-# test-runner-run/run1/script-args: --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
+# test-runner-runs:
+# run1:
+# app: ${ALL_CLUSTERS_APP}
+# factoryreset: true
+# quiet: true
+# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# script-args: >
+# --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
# === END CI TEST ARGUMENTS ===
import chip.clusters as Clusters
diff --git a/src/python_testing/TC_AccessChecker.py b/src/python_testing/TC_AccessChecker.py
index 36c6434..a2429ee 100644
--- a/src/python_testing/TC_AccessChecker.py
+++ b/src/python_testing/TC_AccessChecker.py
@@ -2,12 +2,19 @@
# for details about the block below.
#
# === BEGIN CI TEST ARGUMENTS ===
-# test-runner-runs: run1
-# test-runner-run/run1/app: ${ALL_CLUSTERS_APP}
-# test-runner-run/run1/factoryreset: True
-# test-runner-run/run1/quiet: True
-# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
-# test-runner-run/run1/script-args: --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
+# test-runner-runs:
+# run1:
+# app: ${ALL_CLUSTERS_APP}
+# factoryreset: true
+# quiet: true
+# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# script-args: >
+# --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
# === END CI TEST ARGUMENTS ===
import logging
diff --git a/src/python_testing/matter_testing_infrastructure/chip/testing/metadata.py b/src/python_testing/matter_testing_infrastructure/chip/testing/metadata.py
index aa829c4..803160e 100644
--- a/src/python_testing/matter_testing_infrastructure/chip/testing/metadata.py
+++ b/src/python_testing/matter_testing_infrastructure/chip/testing/metadata.py
@@ -12,17 +12,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import logging
import re
-import sys
from dataclasses import dataclass
+from io import StringIO
from typing import Any, Dict, List
import yaml
-def bool_from_str(value: str) -> bool:
+# TODO #35787: Remove support for non-YAML format
+def cast_to_bool(value: Any) -> bool:
"""Convert True/true/False/false strings to bool."""
- return value.strip().lower() == "true"
+ if isinstance(value, str):
+ return value.strip().lower() == "true"
+ return bool(value)
@dataclass
@@ -67,16 +71,22 @@
self.py_script_path = attr_dict["py_script_path"]
if "factoryreset" in attr_dict:
- self.factoryreset = bool_from_str(attr_dict["factoryreset"])
+ self.factoryreset = cast_to_bool(attr_dict["factoryreset"])
if "factoryreset_app_only" in attr_dict:
- self.factoryreset_app_only = bool_from_str(attr_dict["factoryreset_app_only"])
+ self.factoryreset_app_only = cast_to_bool(attr_dict["factoryreset_app_only"])
if "script_gdb" in attr_dict:
- self.script_gdb = bool_from_str(attr_dict["script_gdb"])
+ self.script_gdb = cast_to_bool(attr_dict["script_gdb"])
if "quiet" in attr_dict:
- self.quiet = bool_from_str(attr_dict["quiet"])
+ self.quiet = cast_to_bool(attr_dict["quiet"])
+
+
+class NamedStringIO(StringIO):
+ def __init__(self, content, name):
+ super().__init__(content)
+ self.name = name
def extract_runs_arg_lines(py_script_path: str) -> Dict[str, Dict[str, str]]:
@@ -91,23 +101,34 @@
runs_arg_lines: Dict[str, Dict[str, str]] = {}
+ ci_args_section_lines = []
with open(py_script_path, 'r', encoding='utf8') as py_script:
for line_idx, line in enumerate(py_script.readlines()):
line = line.strip()
line_num = line_idx + 1
+ # Append empty line to the line capture, so during YAML parsing
+ # line numbers will match the original file.
+ ci_args_section_lines.append("")
+
# Detect the single CI args section, to skip the lines otherwise.
if not done_ci_args_section and line.startswith("# === BEGIN CI TEST ARGUMENTS ==="):
found_ci_args_section = True
+ continue
elif found_ci_args_section and line.startswith("# === END CI TEST ARGUMENTS ==="):
done_ci_args_section = True
found_ci_args_section = False
+ continue
+
+ if found_ci_args_section:
+ # Update the last line in the line capture.
+ ci_args_section_lines[-1] = " " + line.lstrip("#")
runs_match = runs_def_ptrn.match(line)
args_match = arg_def_ptrn.match(line)
if not found_ci_args_section and (runs_match or args_match):
- print(f"WARNING: {py_script_path}:{line_num}: Found CI args outside of CI TEST ARGUMENTS block!", file=sys.stderr)
+ logging.warning(f"{py_script_path}:{line_num}: Found CI args outside of CI TEST ARGUMENTS block")
continue
if runs_match:
@@ -119,6 +140,17 @@
elif args_match:
runs_arg_lines[args_match.group("run_id")][args_match.group("arg_name")] = args_match.group("arg_val")
+ if not runs_arg_lines:
+ try:
+ runs = yaml.safe_load(NamedStringIO("\n".join(ci_args_section_lines), py_script_path))
+ for run, args in runs.get("test-runner-runs", {}).items():
+ runs_arg_lines[run] = {}
+ runs_arg_lines[run]['run'] = run
+ runs_arg_lines[run]['py_script_path'] = py_script_path
+ runs_arg_lines[run].update(args)
+ except yaml.YAMLError as e:
+ logging.error(f"Failed to parse CI arguments YAML: {e}")
+
return runs_arg_lines
@@ -155,10 +187,12 @@
the value for that argument defined in the test script.
"""
for arg, arg_val in metadata_dict.items():
+ if not isinstance(arg_val, str):
+ continue
# We do not expect to recurse (like ${FOO_${BAR}}) so just expand once
for name, value in self.env.items():
arg_val = arg_val.replace(f'${{{name}}}', value)
- metadata_dict[arg] = arg_val
+ metadata_dict[arg] = arg_val.strip()
def parse_script(self, py_script_path: str) -> List[Metadata]:
"""
diff --git a/src/python_testing/matter_testing_infrastructure/chip/testing/test_metadata.py b/src/python_testing/matter_testing_infrastructure/chip/testing/test_metadata.py
index 863add9..ce41969 100644
--- a/src/python_testing/matter_testing_infrastructure/chip/testing/test_metadata.py
+++ b/src/python_testing/matter_testing_infrastructure/chip/testing/test_metadata.py
@@ -34,6 +34,21 @@
# test-runner-run/run1/quiet: False
'''
+ test_file_content_yaml = '''
+ # === BEGIN CI TEST ARGUMENTS ===
+ # test-runner-runs:
+ # run1:
+ # app: ${ALL_CLUSTERS_APP}
+ # app-args: --discriminator 1234 --trace-to json:${TRACE_APP}.json
+ # script-args: >
+ # --commissioning-method on-network
+ # --trace-to json:${TRACE_TEST_JSON}.json
+ # --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+ # factoryreset: true
+ # quiet: true
+ # === END CI TEST ARGUMENTS ===
+ '''
+
env_file_content = '''
ALL_CLUSTERS_APP: out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app
CHIP_LOCK_APP: out/linux-x64-lock-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-lock-app
@@ -61,15 +76,21 @@
def test_run_arg_generation(self):
with tempfile.TemporaryDirectory() as temp_dir:
- temp_file = self.generate_temp_file(temp_dir, self.test_file_content)
+ test_file = self.generate_temp_file(temp_dir, self.test_file_content)
env_file = self.generate_temp_file(temp_dir, self.env_file_content)
reader = MetadataReader(env_file)
- self.maxDiff = None
+ self.expected_metadata.py_script_path = test_file
+ self.assertEqual(self.expected_metadata, reader.parse_script(test_file)[0])
- self.expected_metadata.py_script_path = temp_file
- actual = reader.parse_script(temp_file)[0]
- self.assertEqual(self.expected_metadata, actual)
+ def test_run_arg_generation_yaml(self):
+ with tempfile.TemporaryDirectory() as temp_dir:
+ test_file = self.generate_temp_file(temp_dir, self.test_file_content_yaml)
+ env_file = self.generate_temp_file(temp_dir, self.env_file_content)
+
+ reader = MetadataReader(env_file)
+ self.expected_metadata.py_script_path = test_file
+ self.assertEqual(self.expected_metadata, reader.parse_script(test_file)[0])
if __name__ == "__main__":