diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 0188ebf2524..bf9d42e9f56 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -46,7 +46,7 @@ jobs: run: | python -m pip install pytest # Standalone CI tool tests; skip repo-root conftest.py (imports cuda.pathfinder). - python -m pytest -v --noconftest ci/tools/tests + python -m pytest -v --noconftest ci/tools/tests toolshed/tests find-wheels: runs-on: ubuntu-latest diff --git a/toolshed/run_stubgen_pyx.py b/toolshed/run_stubgen_pyx.py index 1a163ff0778..e758ba370cb 100644 --- a/toolshed/run_stubgen_pyx.py +++ b/toolshed/run_stubgen_pyx.py @@ -30,10 +30,15 @@ def _normalize_stub_headers(root: pathlib.Path) -> None: for stub in root.rglob("*.pyi"): data = stub.read_bytes() newline = data.find(b"\n") + # ``find`` returns -1 when the stub is a single line with no trailing + # newline. Both slices must honour that sentinel: ``data[-1:]`` is the + # file's last byte, so the rest-of-file slice would append a duplicate + # of it to the rewritten header. first_line = data[:newline] if newline != -1 else data + rest = data[newline:] if newline != -1 else b"" if not first_line.startswith(_HEADER_PREFIX) or b"\\" not in first_line: continue - stub.write_bytes(first_line.replace(b"\\", b"/") + data[newline:]) + stub.write_bytes(first_line.replace(b"\\", b"/") + rest) def main() -> int: diff --git a/toolshed/tests/test_run_stubgen_pyx.py b/toolshed/tests/test_run_stubgen_pyx.py new file mode 100644 index 00000000000..f61ac82b654 --- /dev/null +++ b/toolshed/tests/test_run_stubgen_pyx.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from run_stubgen_pyx import _HEADER_PREFIX, _normalize_stub_headers + +WINDOWS_HEADER = _HEADER_PREFIX + b" from cuda_core\\cuda\\core\\_device.pyx" +POSIX_HEADER = _HEADER_PREFIX + b" from cuda_core/cuda/core/_device.pyx" + + +def write_stub(tmp_path, content): + stub = tmp_path / "_device.pyi" + stub.write_bytes(content) + return stub + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize( + ("body", "note"), + [ + pytest.param(b"\n\nclass Device: ...\n", "header, blank line, body", id="body-with-trailing-newline"), + pytest.param(b"\nclass Device: ...", "body without a trailing newline", id="body-no-trailing-newline"), + pytest.param(b"\n", "header line only", id="header-only-with-newline"), + # `.pyi` is excluded from the end-of-file-fixer hook, so a stub with no + # trailing newline at all is a shape this repo tolerates -- and it is + # the one the -1 sentinel from `bytes.find` corrupted: `data[-1:]` + # appended a duplicate of the file's last byte to the header. + pytest.param(b"", "no trailing newline anywhere", id="header-only-no-newline"), + ], +) +def test_separator_is_rewritten_without_touching_anything_else(tmp_path, body, note): + stub = write_stub(tmp_path, WINDOWS_HEADER + body) + + _normalize_stub_headers(tmp_path) + + assert stub.read_bytes() == POSIX_HEADER + body, note + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize( + "content", + [ + pytest.param(POSIX_HEADER, id="already-posix-no-newline"), + pytest.param(POSIX_HEADER + b"\nclass Device: ...\n", id="already-posix"), + pytest.param(b"# hand-written stub\\with\\backslashes", id="not-a-generated-header"), + pytest.param(b"", id="empty-file"), + ], +) +def test_files_that_need_no_rewrite_are_left_byte_identical(tmp_path, content): + stub = write_stub(tmp_path, content) + + _normalize_stub_headers(tmp_path) + + assert stub.read_bytes() == content