diff --git a/CHANGES.md b/CHANGES.md index 408554e..56f7b7b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - Add Renode device support. +- Refuse commands longer than the target CONFIG_LINE_MAX. ## 0.0.3 diff --git a/Documentation/writing-test-cases.rst b/Documentation/writing-test-cases.rst index 1f3a30e..cce9e14 100644 --- a/Documentation/writing-test-cases.rst +++ b/Documentation/writing-test-cases.rst @@ -201,7 +201,9 @@ Decorators: enabled The :meth:`~ntfc.products.ProductsHandler.sendCommand` method handles -execution and verification. +execution and verification. A command that does not fit the target shell +line buffer (``CONFIG_LINE_MAX`` minus one character) is refused with a +``ValueError`` instead of being split by the shell. Parametrized Command Test -------------------------- diff --git a/src/ntfc/core.py b/src/ntfc/core.py index 9de18fc..8f2ddc5 100644 --- a/src/ntfc/core.py +++ b/src/ntfc/core.py @@ -127,6 +127,28 @@ def _match_not_found(self, rematch: Optional[re.Match[Any]]) -> bool: matched = matched.decode("utf-8", errors="ignore") return self._device.no_cmd in matched + def _check_line_max(self, cmd: bytes) -> None: + """Refuse a command the target shell cannot buffer in one line. + + NSH reads at most ``CONFIG_LINE_MAX - 1`` characters per line and + executes the overflow as a second command, which fails in + confusing ways. The limit is only known when the Kconfig of the + image is available. + + :param cmd: Encoded command including its line terminator. + :raises ValueError: Command does not fit the target line buffer. + """ + limit = self._conf.kv_check("CONFIG_LINE_MAX") + if not limit: + return + length = len(cmd.rstrip(b"\r\n")) + if length >= int(limit): + raise ValueError( + f"command is {length} characters, the target buffers at " + f"most {int(limit) - 1} (CONFIG_LINE_MAX={limit}): " + f"{cmd.decode(errors='replace').strip()!r}" + ) + def sendCommand( # noqa: N802 self, cmd: str, @@ -162,6 +184,7 @@ def sendCommand( # noqa: N802 encoded = self._builder.build( cmd, expects, args, flag, match_all, regexp, fail_pattern ) + self._check_line_max(encoded.cmd) logger.debug( f"Sending command: {encoded.cmd.decode()}, expecting: " @@ -204,6 +227,7 @@ def sendCommandReadUntilPattern( # noqa: N802 :return: CmdReturn : command return data """ encoded = self._builder.build_raw(cmd, pattern, args, fail_pattern) + self._check_line_max(encoded.cmd) logger.debug( f"Sending command: {encoded.cmd.decode()}, expecting pattern: " diff --git a/tests/test_core.py b/tests/test_core.py index 7439831..1b4990b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -25,6 +25,7 @@ from ntfc.core import CoreStatus, ProductCore from ntfc.device.common import CmdReturn, CmdStatus +from ntfc.envconfig import EnvConfig def test_core_init(envconfig_dummy): @@ -102,6 +103,40 @@ def test_core_send_command(envconfig_dummy): assert p.sendCommand("test") == CmdStatus.NOTFOUND +def test_core_rejects_command_longer_than_line_max(config_sim): + """A command the shell cannot buffer is refused instead of split.""" + + with patch("ntfc.device.common.DeviceCommon") as mockdevice: + dev = mockdevice.return_value + dev.prompt = b"NSH> " + dev.no_cmd = "command not found" + dev.send_cmd_read_until_pattern.return_value = CmdReturn( + CmdStatus.SUCCESS + ) + core = EnvConfig(config_sim).product[0].cfg_core(0) + assert core.kv_check("CONFIG_LINE_MAX") == 80 + p = ProductCore(dev, core) + + assert p.sendCommand("x" * 79) == CmdStatus.SUCCESS + with pytest.raises(ValueError, match="CONFIG_LINE_MAX"): + p.sendCommand("x" * 80) + with pytest.raises(ValueError, match="CONFIG_LINE_MAX"): + p.sendCommandReadUntilPattern("x" * 70, args=["y" * 10]) + + +def test_core_line_max_unknown_is_not_checked(envconfig_dummy): + + with patch("ntfc.device.common.DeviceCommon") as mockdevice: + dev = mockdevice.return_value + dev.prompt = b"NSH> " + dev.no_cmd = "command not found" + dev.send_cmd_read_until_pattern.return_value = CmdReturn( + CmdStatus.SUCCESS + ) + p = ProductCore(dev, envconfig_dummy.product[0].cfg_core(0)) + assert p.sendCommand("x" * 500) == CmdStatus.SUCCESS + + def test_core_send_command_read_until_pattern(envconfig_dummy): with patch("ntfc.device.common.DeviceCommon") as mockdevice: dev = mockdevice.return_value