From 5ba5c730f058d74b9ca81d58b70833b66d7dd4db Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 14 Aug 2025 00:06:16 +0100 Subject: [PATCH 1/5] Test wait_for_proc timeout --- nixie/cli.py | 22 +++++++---------- nixie/unittests/test_puppeteer_config.py | 6 +++++ nixie/unittests/test_wait_for_proc.py | 30 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 nixie/unittests/test_wait_for_proc.py diff --git a/nixie/cli.py b/nixie/cli.py index b6b2d0f..026978a 100644 --- a/nixie/cli.py +++ b/nixie/cli.py @@ -146,7 +146,7 @@ async def wait_for_proc( """Wait for a process to complete and return its success status and stderr.""" try: _, stderr = await asyncio.wait_for(proc.communicate(), timeout) - except TimeoutError: + except asyncio.TimeoutError: # noqa: UP041 proc.kill() await proc.wait() print(f"{path}: diagram {idx} timed out", file=sys.stderr) @@ -172,8 +172,7 @@ async def _run_mermaid_cli( stdout=asyncio_subprocess.PIPE, stderr=asyncio_subprocess.PIPE, ) - - return await wait_for_proc(proc, path, idx, timeout) + return await wait_for_proc(proc, path, idx, timeout) async def _render_diagram( @@ -219,18 +218,8 @@ async def _render_diagram( mmd.write_text(block) cmd = get_mmdc_cmd(mmd, svg, cfg_path) - if not cmd or cmd[0] not in ALLOWED_EXECUTABLES: - raise UnexpectedExecutableError(cmd[0] if cmd else "") LOGGER.info(shlex.join(cmd)) - - async with semaphore: - # nosemgrep: python.lang.security.audit.dangerous-asyncio-create-exec-audit - proc = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - success, stderr = await wait_for_proc(proc, path, idx, timeout) + success, stderr = await _run_mermaid_cli(cmd, semaphore, path, idx, timeout) if not success: error_message = ( f"Error running command {shlex.join(cmd)} for file '{path}' " @@ -299,6 +288,11 @@ async def render_block( ), cli, ) + except NoNodeEnvironmentAvailableError: + LOGGER.exception( + "No supported node environment found. Install mmdc directly, or install " + "Node.js (npx) or Bun to use @mermaid-js/mermaid-cli." + ) except RuntimeError: LOGGER.exception("Runtime error while rendering diagram") except Exception as exc: diff --git a/nixie/unittests/test_puppeteer_config.py b/nixie/unittests/test_puppeteer_config.py index 7921b13..bb89472 100644 --- a/nixie/unittests/test_puppeteer_config.py +++ b/nixie/unittests/test_puppeteer_config.py @@ -9,16 +9,22 @@ from nixie.cli import create_puppeteer_config if typ.TYPE_CHECKING: + from pathlib import Path + import pytest def test_create_puppeteer_config_as_root(monkeypatch: pytest.MonkeyPatch) -> None: """Include sandbox-disabling args when running as root.""" monkeypatch.setattr(os, "geteuid", lambda: 0) + path: Path | None = None with create_puppeteer_config() as cfg: assert cfg is not None + path = cfg data = json.loads(cfg.read_text()) assert data["args"] == ["--no-sandbox", "--disable-setuid-sandbox"] + assert path is not None + assert not path.exists() def test_create_puppeteer_config_non_root(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/nixie/unittests/test_wait_for_proc.py b/nixie/unittests/test_wait_for_proc.py new file mode 100644 index 0000000..e3ea4d9 --- /dev/null +++ b/nixie/unittests/test_wait_for_proc.py @@ -0,0 +1,30 @@ +"""Tests for :func:`nixie.cli.wait_for_proc`.""" + +from __future__ import annotations + +import asyncio +import asyncio.subprocess as asyncio_subprocess +import sys +import typing as typ + +import pytest + +from nixie.cli import wait_for_proc + +if typ.TYPE_CHECKING: + from pathlib import Path + + +@pytest.mark.asyncio +async def test_wait_for_proc_times_out(tmp_path: Path) -> None: + """Return ``False`` when the process exceeds ``timeout``.""" + proc = await asyncio.create_subprocess_exec( + sys.executable, + "-c", + "import time; time.sleep(1)", + stdout=asyncio_subprocess.PIPE, + stderr=asyncio_subprocess.PIPE, + ) + success, stderr = await wait_for_proc(proc, tmp_path / "dummy.md", 1, timeout=0.01) + assert not success + assert stderr == b"" From 18b9cd81c63fcbac701fea2ad6d017faab1c124a Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 14 Aug 2025 12:28:09 +0100 Subject: [PATCH 2/5] Test wait_for_proc handles TimeoutError --- nixie/unittests/test_wait_for_proc.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/nixie/unittests/test_wait_for_proc.py b/nixie/unittests/test_wait_for_proc.py index e3ea4d9..8fc69f7 100644 --- a/nixie/unittests/test_wait_for_proc.py +++ b/nixie/unittests/test_wait_for_proc.py @@ -16,12 +16,22 @@ @pytest.mark.asyncio -async def test_wait_for_proc_times_out(tmp_path: Path) -> None: - """Return ``False`` when the process exceeds ``timeout``.""" +async def test_wait_for_proc_handles_asyncio_timeout_error(tmp_path: Path) -> None: + """Handle :class:`asyncio.TimeoutError` raised by ``asyncio.wait_for``.""" + cmd = [sys.executable, "-c", "import time; time.sleep(1)"] + + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio_subprocess.PIPE, + stderr=asyncio_subprocess.PIPE, + ) + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(proc.communicate(), 0.01) + proc.kill() + await proc.wait() + proc = await asyncio.create_subprocess_exec( - sys.executable, - "-c", - "import time; time.sleep(1)", + *cmd, stdout=asyncio_subprocess.PIPE, stderr=asyncio_subprocess.PIPE, ) From a943c7d7d60cb9bbae8ffb1a63f42d093a0b8982 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 14 Aug 2025 21:05:36 +0100 Subject: [PATCH 3/5] Log missing node env without traceback --- nixie/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixie/cli.py b/nixie/cli.py index 026978a..0bf7187 100644 --- a/nixie/cli.py +++ b/nixie/cli.py @@ -289,7 +289,7 @@ async def render_block( cli, ) except NoNodeEnvironmentAvailableError: - LOGGER.exception( + LOGGER.error( # noqa: TRY400 "No supported node environment found. Install mmdc directly, or install " "Node.js (npx) or Bun to use @mermaid-js/mermaid-cli." ) From 85edee9c5a90e371f8738d399a00a2b997329131 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 15 Aug 2025 13:41:40 +0100 Subject: [PATCH 4/5] Clarify noqa suppressions --- nixie/cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nixie/cli.py b/nixie/cli.py index 0bf7187..687125a 100644 --- a/nixie/cli.py +++ b/nixie/cli.py @@ -146,7 +146,7 @@ async def wait_for_proc( """Wait for a process to complete and return its success status and stderr.""" try: _, stderr = await asyncio.wait_for(proc.communicate(), timeout) - except asyncio.TimeoutError: # noqa: UP041 + except asyncio.TimeoutError: # noqa: UP041 # https://github.com/astral-sh/ruff/issues/8565 proc.kill() await proc.wait() print(f"{path}: diagram {idx} timed out", file=sys.stderr) @@ -289,9 +289,10 @@ async def render_block( cli, ) except NoNodeEnvironmentAvailableError: - LOGGER.error( # noqa: TRY400 + LOGGER.error( # noqa: TRY400 # user-facing error; suppress stack trace "No supported node environment found. Install mmdc directly, or install " - "Node.js (npx) or Bun to use @mermaid-js/mermaid-cli." + "Node.js (npx) or Bun to use @mermaid-js/mermaid-cli.", + exc_info=False, ) except RuntimeError: LOGGER.exception("Runtime error while rendering diagram") From 0737f94b4861967dcca25bd9b5577236af73824a Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 15 Aug 2025 14:09:07 +0100 Subject: [PATCH 5/5] Clarify asyncio timeout suppression --- nixie/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixie/cli.py b/nixie/cli.py index 687125a..dff6a04 100644 --- a/nixie/cli.py +++ b/nixie/cli.py @@ -146,7 +146,7 @@ async def wait_for_proc( """Wait for a process to complete and return its success status and stderr.""" try: _, stderr = await asyncio.wait_for(proc.communicate(), timeout) - except asyncio.TimeoutError: # noqa: UP041 # https://github.com/astral-sh/ruff/issues/8565 + except asyncio.TimeoutError: # noqa: UP041 # TODO(leynos): remove once ruff issue 8565 is fixed https://github.com/astral-sh/ruff/issues/8565 proc.kill() await proc.wait() print(f"{path}: diagram {idx} timed out", file=sys.stderr)