diff --git a/nixie/cli.py b/nixie/cli.py index b6b2d0f..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 TimeoutError: + 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) @@ -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,12 @@ async def render_block( ), cli, ) + except NoNodeEnvironmentAvailableError: + 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.", + exc_info=False, + ) 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..8fc69f7 --- /dev/null +++ b/nixie/unittests/test_wait_for_proc.py @@ -0,0 +1,40 @@ +"""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_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( + *cmd, + 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""