Skip to content
Merged
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
4 changes: 3 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@
"CNET",
"AVSM",
"PAVST",
"WEBRTCR"
"WEBRTCR",
"ZONEMGMT",
"APPDEVTYPEID"
],
"allowCompoundWords": true,
"ignorePaths": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from __future__ import annotations

import json
from pathlib import Path
from typing import Generator, cast

Expand Down Expand Up @@ -57,6 +58,13 @@
DutPairingModeEnum.NFC_THREAD.value,
}

# Typed SDK argument flags that accept NAME:VALUE pairs and require special
# handling to survive the container shell without mangling.
_SPLIT_ARGS = {"json-arg", "string-arg", "int-arg", "float-arg", "bool-arg", "hex-arg"}

# Characters that trigger shell brace expansion or word splitting.
_SHELL_SPECIAL = set("{[}\"'")


async def generate_command_arguments(
config: TestEnvironmentConfigMatter, omit_commissioning_method: bool = False
Expand Down Expand Up @@ -123,11 +131,36 @@ async def generate_command_arguments(
arguments.append(f"--discriminator {dut_config.discriminator}")
arguments.append(f"--passcode {dut_config.setup_code}")

# Retrieve arguments from test_parameters
# Retrieve arguments from test_parameters.
# Typed SDK args (--json-arg, --string-arg, etc.) may contain special
# characters such as braces and quotes that the container shell would
# mangle if embedded in a single string token.
# Emit the flag and each NAME:VALUE pair as separate list entries.
# Single-quote pairs that contain shell-special characters so the
# container shell does not perform brace expansion or word splitting.
# json-arg is always a single NAME:JSON token — never split on spaces,
# as the JSON value itself may contain spaces.
# Other typed args (int-arg, string-arg, etc.) use plain NAME:VALUE pairs
# with no spaces, so splitting on spaces is safe for them.
if test_parameters:
for name, value in test_parameters.items():
arg_value = str(value) if value is not None else ""
arguments.append(f"--{name} {arg_value}")
if isinstance(value, (dict, list)):
arg_value = json.dumps(value, separators=(",", ":"))
elif value is not None:
arg_value = str(value)
else:
arg_value = ""
if name in _SPLIT_ARGS:
arguments.append(f"--{name}")
pairs = [arg_value] if name == "json-arg" else arg_value.split(" ")
for pair in pairs:
if pair:
if any(c in pair for c in _SHELL_SPECIAL):
arguments.append(f"'{pair}'")
else:
arguments.append(pair)
else:
arguments.append(f"--{name} {arg_value}")

return arguments

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from app.test_engine.logger import test_engine_logger
from test_collections.matter.test_environment_config import (
DutConfig,
TestEnvironmentConfigMatter,
ThreadExternalConfig,
)

Expand All @@ -35,6 +36,24 @@
)
from ...sdk_container import SDKContainer

# ---------------------------------------------------------------------------
# Helpers shared by the new json-arg / typed-arg tests
# ---------------------------------------------------------------------------


def _on_network_config(test_parameters: dict) -> TestEnvironmentConfigMatter:
"""Return a deep-copied default config with ON_NETWORK pairing and the
given test_parameters dict."""
cfg = default_environment_config.copy(deep=True) # type: ignore
cfg.dut_config = DutConfig(
discriminator="3840",
setup_code="20202021",
pairing_mode=DutPairingModeEnum.ON_NETWORK,
chip_timeout=None,
)
cfg.test_parameters = test_parameters
return cfg


@pytest.mark.asyncio
async def test_generate_command_arguments_with_null_value_attribute() -> None:
Expand Down Expand Up @@ -542,3 +561,232 @@ async def test_commission_device_failure() -> None:
expected_command, prefix=EXECUTABLE, is_stream=True, is_socket=False
)
mock_handle_logs.assert_called_once()


# ---------------------------------------------------------------------------
# Tests for the new typed-arg / json-arg handling in generate_command_arguments
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_generate_command_arguments_json_arg_string_value() -> None:
"""json-arg with a plain JSON string value is emitted as two separate list
entries: '--json-arg' and the single-quoted NAME:JSON pair."""
json_value = 'PIXIT.ZONEMGMT.Zone1:{"vertices":[{"x":0,"y":0},{"x":100,"y":100}]}'
cfg = _on_network_config({"json-arg": json_value})

arguments = await generate_command_arguments(cfg)

assert "--json-arg" in arguments
idx = arguments.index("--json-arg")
assert arguments[idx + 1] == f"'{json_value}'"


@pytest.mark.asyncio
async def test_generate_command_arguments_json_arg_dict_value() -> None:
"""json-arg with a dict value (Pydantic parsed it as a nested object) is
serialized to compact JSON and single-quoted."""
cfg = _on_network_config({"json-arg": "PIXIT.X:1"})
# Override with a dict value to exercise the json.dumps path
cfg.test_parameters = {
"json-arg": {"vertices": [{"x": 0, "y": 0}, {"x": 100, "y": 100}]}
}

arguments = await generate_command_arguments(cfg)

assert "--json-arg" in arguments
idx = arguments.index("--json-arg")
# dict serialized to compact JSON and single-quoted because it contains {
assert arguments[idx + 1] == '\'{"vertices":[{"x":0,"y":0},{"x":100,"y":100}]}\''


@pytest.mark.asyncio
async def test_generate_command_arguments_int_arg_single_pair() -> None:
"""int-arg with a single NAME:VALUE pair (no special chars) is NOT
single-quoted."""
cfg = _on_network_config({"int-arg": "PIXIT.ACE.APPENDPOINT:1"})

arguments = await generate_command_arguments(cfg)

assert "--int-arg" in arguments
idx = arguments.index("--int-arg")
assert arguments[idx + 1] == "PIXIT.ACE.APPENDPOINT:1"


@pytest.mark.asyncio
async def test_generate_command_arguments_int_arg_multiple_pairs() -> None:
"""int-arg with multiple space-separated NAME:VALUE pairs produces one
'--int-arg' flag followed by each pair as a separate list entry."""
cfg = _on_network_config(
{"int-arg": "PIXIT.ACE.APPENDPOINT:1 PIXIT.ACE.APPDEVTYPEID:256"}
)

arguments = await generate_command_arguments(cfg)

assert "--int-arg" in arguments
idx = arguments.index("--int-arg")
assert arguments[idx + 1] == "PIXIT.ACE.APPENDPOINT:1"
assert arguments[idx + 2] == "PIXIT.ACE.APPDEVTYPEID:256"


@pytest.mark.asyncio
async def test_generate_command_arguments_string_arg_multiple_pairs() -> None:
"""string-arg with multiple space-separated NAME:VALUE pairs is split into
individual list entries, none of which are quoted (no special chars)."""
cfg = _on_network_config(
{"string-arg": "PIXIT.ACE.APPCLUSTER:OnOff PIXIT.ACE.APPATTRIBUTE:OnOff"}
)

arguments = await generate_command_arguments(cfg)

assert "--string-arg" in arguments
idx = arguments.index("--string-arg")
assert arguments[idx + 1] == "PIXIT.ACE.APPCLUSTER:OnOff"
assert arguments[idx + 2] == "PIXIT.ACE.APPATTRIBUTE:OnOff"


@pytest.mark.asyncio
async def test_generate_command_arguments_json_arg_and_int_arg_together() -> None:
"""json-arg and int-arg can coexist; each is handled independently."""
cfg = _on_network_config(
{
"json-arg": 'PIXIT.ZONE:{"vertices":[{"x":0,"y":0}]}',
"int-arg": "PIXIT.ACE.APPENDPOINT:1",
}
)

arguments = await generate_command_arguments(cfg)

assert "--json-arg" in arguments
assert "--int-arg" in arguments

json_idx = arguments.index("--json-arg")
assert arguments[json_idx + 1] == '\'PIXIT.ZONE:{"vertices":[{"x":0,"y":0}]}\''

int_idx = arguments.index("--int-arg")
assert arguments[int_idx + 1] == "PIXIT.ACE.APPENDPOINT:1"


@pytest.mark.asyncio
async def test_generate_command_arguments_non_split_arg_unchanged() -> None:
"""Args not in _SPLIT_ARGS (e.g. paa-trust-store-path) are still emitted
as a single '--flag value' string, unchanged by the new logic."""
cfg = _on_network_config({"paa-trust-store-path": "/paa-root-certs"})

arguments = await generate_command_arguments(cfg)

assert "--paa-trust-store-path /paa-root-certs" in arguments


@pytest.mark.asyncio
async def test_generate_command_arguments_json_arg_value_with_spaces() -> None:
"""json-arg where the JSON value contains spaces is kept as a single token.
This validates that shlex.split() is used instead of a naive split(' ')."""
json_value = 'PIXIT.Key:{"name":"some name with spaces"}'
cfg = _on_network_config({"json-arg": json_value})

arguments = await generate_command_arguments(cfg)

assert "--json-arg" in arguments
idx = arguments.index("--json-arg")
assert arguments[idx + 1] == f"'{json_value}'"


@pytest.mark.asyncio
async def test_generate_command_arguments_json_arg_null_value() -> None:
"""json-arg with a None value emits '--json-arg' with no following value
(empty string is skipped by the pair loop)."""
cfg = _on_network_config({"json-arg": None})

arguments = await generate_command_arguments(cfg)

assert "--json-arg" in arguments
idx = arguments.index("--json-arg")
# Nothing follows --json-arg when the value is None/empty
assert idx == len(arguments) - 1
Comment thread
rquidute marked this conversation as resolved.


@pytest.mark.asyncio
async def test_generate_command_arguments_float_arg_single_pair() -> None:
"""float-arg with a single NAME:VALUE pair (no special chars) is NOT
single-quoted."""
cfg = _on_network_config({"float-arg": "PIXIT.SENSOR.TOLERANCE:0.5"})

arguments = await generate_command_arguments(cfg)

assert "--float-arg" in arguments
idx = arguments.index("--float-arg")
assert arguments[idx + 1] == "PIXIT.SENSOR.TOLERANCE:0.5"


@pytest.mark.asyncio
async def test_generate_command_arguments_float_arg_multiple_pairs() -> None:
"""float-arg with multiple space-separated NAME:VALUE pairs produces one
'--float-arg' flag followed by each pair as a separate list entry."""
cfg = _on_network_config(
{"float-arg": "PIXIT.SENSOR.MIN:0.0 PIXIT.SENSOR.MAX:100.0"}
)

arguments = await generate_command_arguments(cfg)

assert "--float-arg" in arguments
idx = arguments.index("--float-arg")
assert arguments[idx + 1] == "PIXIT.SENSOR.MIN:0.0"
assert arguments[idx + 2] == "PIXIT.SENSOR.MAX:100.0"


@pytest.mark.asyncio
async def test_generate_command_arguments_bool_arg_single_pair() -> None:
"""bool-arg with a single NAME:VALUE pair is NOT single-quoted."""
cfg = _on_network_config({"bool-arg": "PIXIT.TEST.ENABLED:True"})

arguments = await generate_command_arguments(cfg)

assert "--bool-arg" in arguments
idx = arguments.index("--bool-arg")
assert arguments[idx + 1] == "PIXIT.TEST.ENABLED:True"


@pytest.mark.asyncio
async def test_generate_command_arguments_bool_arg_multiple_pairs() -> None:
"""bool-arg with multiple space-separated NAME:VALUE pairs produces one
'--bool-arg' flag followed by each pair as a separate list entry."""
cfg = _on_network_config(
{"bool-arg": "PIXIT.TEST.FEATURE_A:True PIXIT.TEST.FEATURE_B:False"}
)

arguments = await generate_command_arguments(cfg)

assert "--bool-arg" in arguments
idx = arguments.index("--bool-arg")
assert arguments[idx + 1] == "PIXIT.TEST.FEATURE_A:True"
assert arguments[idx + 2] == "PIXIT.TEST.FEATURE_B:False"


@pytest.mark.asyncio
async def test_generate_command_arguments_hex_arg_single_pair() -> None:
"""hex-arg with a single NAME:VALUE pair (no special chars) is NOT
single-quoted."""
cfg = _on_network_config({"hex-arg": "PIXIT.COMMISSIONING.DATASET:DEADBEEF"})

arguments = await generate_command_arguments(cfg)

assert "--hex-arg" in arguments
idx = arguments.index("--hex-arg")
assert arguments[idx + 1] == "PIXIT.COMMISSIONING.DATASET:DEADBEEF"


@pytest.mark.asyncio
async def test_generate_command_arguments_hex_arg_multiple_pairs() -> None:
"""hex-arg with multiple space-separated NAME:VALUE pairs produces one
'--hex-arg' flag followed by each pair as a separate list entry."""
cfg = _on_network_config(
{"hex-arg": "PIXIT.DATASET.ACTIVE:AABBCCDD PIXIT.DATASET.PENDING:11223344"}
)

arguments = await generate_command_arguments(cfg)

assert "--hex-arg" in arguments
idx = arguments.index("--hex-arg")
assert arguments[idx + 1] == "PIXIT.DATASET.ACTIVE:AABBCCDD"
assert arguments[idx + 2] == "PIXIT.DATASET.PENDING:11223344"
Loading