diff --git a/.github/workflows/hipfile-python-bindings.yml b/.github/workflows/hipfile-python-bindings.yml index 189b421369a..dab2f7dd908 100644 --- a/.github/workflows/hipfile-python-bindings.yml +++ b/.github/workflows/hipfile-python-bindings.yml @@ -142,7 +142,7 @@ jobs: /bin/bash -c ' python3.12 -m venv .venv .venv/bin/pip install --upgrade pip - .venv/bin/pip install build + .venv/bin/pip install build pytest ' - name: Build hipFile Python sdist & capture metadata id: sdist-metadata @@ -186,6 +186,15 @@ jobs: /bin/bash -c ' .venv/bin/python -c "import hipfile; print(f\"hipfile {hipfile.__version__} installed successfully\")" ' + - name: Run hipFile Python unit tests + run: | + docker exec \ + -t \ + -w /ais/rocm-systems/projects/hipfile \ + "${AIS_CONTAINER_NAME}" \ + /bin/bash -c ' + .venv/bin/python -m pytest python/tests + ' - name: Copy sdist out of the container run: | docker cp \ diff --git a/projects/hipfile/python/tests/README.md b/projects/hipfile/python/tests/README.md new file mode 100644 index 00000000000..31ef0ec3de0 --- /dev/null +++ b/projects/hipfile/python/tests/README.md @@ -0,0 +1,39 @@ +# hipFile Python binding tests + +Unit tests for the high-level `hipfile` Python API. + +They are fully hermetic: the compiled Cython extension `hipfile._hipfile` is +replaced with a pure-Python fake injected into `sys.modules` before `hipfile` is +imported, so the suite runs on any machine — no ROCm install, GPU, AIS-capable +storage, or build step required. Filesystem access in the `FileHandle` tests is +mocked (`os.open` / `os.close`). + +## Running + +From the repository root within a virtual environment: + +``` +pip install pytest +cd projects/hipfile/python/tests +python3 -m pytest +``` + +## Layout + +| File | Covers | +|------|--------| +| `conftest.py` | Installs the fake `hipfile._hipfile` in `sys.modules` and exposes shared fixtures. Explains why the extension substrate is a *fake* (real ints + lambdas) rather than `Mock`/`MagicMock`. | +| `test_enums.py` | `OpError` / `FileHandleType` — values track the extension, members stay distinct, membership checks. | +| `test_error.py` | `HipFileException` — stored codes and `__str__`, including the `HIP_DRIVER_ERROR` branch. | +| `test_driver.py` | `Driver` — open/close success and error, `use_count` delegation, context-manager open-then-close ordering. | +| `test_buffer.py` | `Buffer` — null rejection, register/deregister success and error, no-op deregister, context manager. | +| `test_file.py` | `FileHandle` — `handle_type` setter guards, fd cleanup on registration failure, idempotent close, and the parametrized read/write return-code contract. | +| `test_properties.py` | `get_version` / `driver_get_properties` — success and error paths. | + +## Notes + +- This is a **unit** suite only — it verifies the Python wrapper's contract, not + real GPU/driver/filesystem I/O. +- Per-test overrides use `unittest.mock.patch.object` on the *consuming* module + (e.g. `hipfile.driver.hipFileDriverOpen`), since each module does + `from hipfile._hipfile import ...` and holds its own reference. diff --git a/projects/hipfile/python/tests/conftest.py b/projects/hipfile/python/tests/conftest.py new file mode 100644 index 00000000000..54fa0b3d930 --- /dev/null +++ b/projects/hipfile/python/tests/conftest.py @@ -0,0 +1,230 @@ +# Copyright (c) Advanced Micro Devices, Inc. All rights reserved. +# +# SPDX-License-Identifier: MIT + +""" +Shared fixtures for the hipFile Python binding tests. + +The whole ``hipfile`` package is unimportable without its compiled Cython +extension ``hipfile._hipfile``: ``hipfile/__init__.py`` imports names from it at +import time, and it in turn needs the hipFile C library plus the HIP runtime. To +keep the suite hermetic -- no GPU, no AIS-capable storage, no build step -- we +register a pure-Python fake ``hipfile._hipfile`` in ``sys.modules`` *before* any +test module imports ``hipfile``. Because pytest imports this file during +collection, the injection below runs first. + +Why a fake (real ints + lambdas) rather than ``Mock``/``MagicMock`` for this +module-level substrate: + +* **A correctly-behaving mock is just a decorated fake.** The code needs real + ints from the enums (``IntEnum`` collapses members that share a value) and + correctly-shaped tuples from the callables (``err[0] != 0``, + ``ver, err = hipFileGetVersion()``). Getting those out of mocks means + hand-configuring ``__int__`` on ~40 members and ``return_value`` on ~12 + callables -- reconstructing this fake with more ceremony and nothing for + ``autospec`` to check. +* **A fake carries no cross-test state.** This object lives in ``sys.modules`` + for the whole session. A ``Mock`` would accumulate call history there and + retain any per-test ``return_value``/``side_effect``, so every test would have + to patch it just to get a clean baseline. The fake records nothing, so tests + that don't care about a call need no setup, and tests that do use + ``unittest.mock.patch.object`` locally (auto-reverted at block exit). +""" + +import sys +import types +from pathlib import Path + +import pytest + +# The pure-Python ``hipfile`` package lives one directory up (``python/``). +# Put it on sys.path so ``import hipfile`` resolves without an editable install +# (which would require building the Cython extension we are deliberately faking). +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +# --- Fake extension construction ------------------------------------------ + +# Fixed, arbitrary version so tests can assert a known value. +_FAKE_VERSION = (1, 2, 3) + +# Every hipFileOpError_t name re-exported by hipfile/enums.py, in C order. +# enums.py builds an IntEnum from these, so each MUST get a UNIQUE int -- a +# collision would make the second name an IntEnum *alias* rather than a distinct +# member, silently changing len(OpError) and membership behavior. Success is 0 +# because the high-level API tests ``err[0] != 0`` for failure. +_OP_ERROR_NAMES = ( + "hipFileSuccess", + "hipFileDriverNotInitialized", + "hipFileDriverInvalidProps", + "hipFileDriverUnsupportedLimit", + "hipFileDriverVersionMismatch", + "hipFileDriverVersionReadError", + "hipFileDriverClosing", + "hipFilePlatformNotSupported", + "hipFileIONotSupported", + "hipFileDeviceNotSupported", + "hipFileDriverError", + "hipFileHipDriverError", + "hipFileHipPointerInvalid", + "hipFileHipMemoryTypeInvalid", + "hipFileHipPointerRangeError", + "hipFileHipContextMismatch", + "hipFileInvalidMappingSize", + "hipFileInvalidMappingRange", + "hipFileInvalidFileType", + "hipFileInvalidFileOpenFlag", + "hipFileDIONotSet", + "hipFileInvalidValue", + "hipFileMemoryAlreadyRegistered", + "hipFileMemoryNotRegistered", + "hipFilePermissionDenied", + "hipFileDriverAlreadyOpen", + "hipFileHandleNotRegistered", + "hipFileHandleAlreadyRegistered", + "hipFileDeviceNotFound", + "hipFileInternalError", + "hipFileGetNewFDFailed", + "hipFileDriverSetupError", + "hipFileIODisabled", + "hipFileBatchSubmitFailed", + "hipFileGPUMemoryPinningFailed", + "hipFileBatchFull", + "hipFileAsyncNotSupported", + "hipFileIOMaxError", +) + +# hipFileFileHandleType_t names re-exported by enums.py -- also need unique ints. +_HANDLE_TYPE_NAMES = ( + "hipFileHandleTypeOpaqueFD", + "hipFileHandleTypeOpaqueWin32", + "hipFileHandleTypeUserspaceFS", +) + +# Sentinel returned by hipFileHandleRegister so tests can assert the FileHandle +# stored exactly what the extension handed back. +_FAKE_HANDLE = 0xF11E + +_FAKE_PROPS = { + "nvfs_major_version": 1, + "nvfs_minor_version": 0, + "nvfs_poll_thresh_size": 0, + "nvfs_max_direct_io_size": 0, + "nvfs_driver_status_flags": 0, + "nvfs_driver_control_flags": 0, + "feature_flags": 0, + "max_device_cache_size": 0, + "per_buffer_cache_size": 0, + "max_device_pinned_mem_size": 0, + "max_batch_io_count": 0, + "max_batch_io_timeout_msecs": 0, +} + + +def _build_fake_hipfile(): + """Create a fake ``hipfile._hipfile`` module. + + Callable signatures mirror the real Cython wrappers so that + ``patch.object(..., autospec=True)`` in tests enforces call arity. Defaults + are all success-shaped; error-path tests replace individual callables. + """ + mod = types.ModuleType("hipfile._hipfile") + + # Version constants. + mod.VERSION_MAJOR, mod.VERSION_MINOR, mod.VERSION_PATCH = _FAKE_VERSION + + # Enum values -- unique ints, Success == 0. + for value, name in enumerate(_OP_ERROR_NAMES): + setattr(mod, name, value) + for value, name in enumerate(_HANDLE_TYPE_NAMES): + setattr(mod, name, value) + + # hipFileSuccess is set dynamically via setattr above, so pylint can't see it. + _success = (mod.hipFileSuccess, 0) # pylint: disable=no-member + + # Driver lifecycle. + mod.hipFileDriverOpen = lambda: _success + mod.hipFileDriverClose = lambda: _success + mod.hipFileUseCount = lambda: 0 + + # Version / properties. + mod.hipFileGetVersion = lambda: (_FAKE_VERSION, _success) + mod.hipFileDriverGetProperties = lambda: (dict(_FAKE_PROPS), _success) + + # File handles. + mod.hipFileHandleRegister = lambda handle_value, handle_type: ( + _FAKE_HANDLE, + _success, + ) + mod.hipFileHandleDeregister = lambda handle: None + + # Buffer registration. + mod.hipFileBufRegister = lambda buffer_base, length, flags=0: _success + mod.hipFileBufDeregister = lambda buffer_base: _success + + # Synchronous I/O -- default: full transfer, no error. + mod.hipFileRead = lambda handle, buffer_base, size, file_offset, buffer_offset: ( + size, + 0, + ) + mod.hipFileWrite = lambda handle, buffer_base, size, file_offset, buffer_offset: ( + size, + 0, + ) + + # Error strings. + mod.hipFileGetOpErrorString = lambda status: f"fake-op-error-{status}" + + return mod + + +# Inject BEFORE any test imports hipfile. The ``not in sys.modules`` guard only +# avoids clobbering the module if something imported it first. +if "hipfile._hipfile" not in sys.modules: + sys.modules["hipfile._hipfile"] = _build_fake_hipfile() + + +# --- Fixtures -------------------------------------------------------------- + + +@pytest.fixture +def fake_hipfile(): + """The injected fake ``hipfile._hipfile`` module.""" + return sys.modules["hipfile._hipfile"] + + +@pytest.fixture +def fake_handle(): + """The opaque handle value returned by the fake ``hipFileHandleRegister``.""" + return _FAKE_HANDLE + + +@pytest.fixture +def fake_version(): + """The ``(major, minor, patch)`` tuple the fake reports.""" + return _FAKE_VERSION + + +class _FakeVoidP: # pylint: disable=too-few-public-methods + """Minimal stand-in for ``ctypes.c_void_p`` (only ``.value`` is used).""" + + def __init__(self, value): + self.value = value + + +@pytest.fixture +def fake_void_p(): + """Factory for ``ctypes.c_void_p``-like objects (Buffer.from_ctypes_void_p).""" + return _FakeVoidP + + +class _FakeBuffer: # pylint: disable=too-few-public-methods + """Minimal Buffer stand-in exposing ``.ptr`` for FileHandle read/write.""" + + def __init__(self, ptr=0x1000): + self.ptr = ptr + + +@pytest.fixture +def fake_buffer(): + """A Buffer-like object with a ``.ptr`` attribute.""" + return _FakeBuffer() diff --git a/projects/hipfile/python/tests/pytest.ini b/projects/hipfile/python/tests/pytest.ini new file mode 100644 index 00000000000..74ef110bad6 --- /dev/null +++ b/projects/hipfile/python/tests/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = . +python_files = test_*.py diff --git a/projects/hipfile/python/tests/test_buffer.py b/projects/hipfile/python/tests/test_buffer.py new file mode 100644 index 00000000000..6292493e008 --- /dev/null +++ b/projects/hipfile/python/tests/test_buffer.py @@ -0,0 +1,115 @@ +# Copyright (c) Advanced Micro Devices, Inc. All rights reserved. +# +# SPDX-License-Identifier: MIT + +"""Tests for hipfile.buffer.Buffer.""" + +# pylint: disable=missing-function-docstring,redefined-outer-name,protected-access +# pylint: disable=import-error # hipfile not installed in lint env; extension faked + +from unittest import mock + +import pytest + +import hipfile.buffer +from hipfile.buffer import Buffer +from hipfile.enums import OpError +from hipfile.error import HipFileException + +_SUCCESS = (OpError.SUCCESS, 0) +_FAILURE = (OpError.MEMORY_ALREADY_REGISTERED, 0) + + +def test_from_ctypes_void_p_rejects_null(fake_void_p): + with pytest.raises(ValueError): + Buffer.from_ctypes_void_p(fake_void_p(None), 1024, 0) + + +def test_from_ctypes_void_p_forwards_value(fake_void_p): + buf = Buffer.from_ctypes_void_p(fake_void_p(0xDEAD), 1024, 0) + assert buf.ptr == 0xDEAD + + +def test_register_success(): + buf = Buffer(0x1000, 2048, 0) + with mock.patch.object( + hipfile.buffer, "hipFileBufRegister", autospec=True, return_value=_SUCCESS + ) as reg: + buf.register() + reg.assert_called_once_with(0x1000, 2048, 0) + assert buf._registered is True + + +def test_register_raises_on_error(): + buf = Buffer(0x1000, 2048, 0) + with mock.patch.object( + hipfile.buffer, "hipFileBufRegister", autospec=True, return_value=_FAILURE + ): + with pytest.raises(HipFileException): + buf.register() + assert buf._registered is False + + +def test_deregister_success(): + buf = Buffer(0x1000, 2048, 0) + with mock.patch.object( + hipfile.buffer, "hipFileBufRegister", autospec=True, return_value=_SUCCESS + ): + buf.register() + with mock.patch.object( + hipfile.buffer, "hipFileBufDeregister", autospec=True, return_value=_SUCCESS + ) as dereg: + buf.deregister() + dereg.assert_called_once_with(0x1000) + assert buf._registered is False + + +def test_deregister_is_noop_when_not_registered(): + buf = Buffer(0x1000, 2048, 0) + with mock.patch.object( + hipfile.buffer, "hipFileBufDeregister", autospec=True, return_value=_SUCCESS + ) as dereg: + buf.deregister() + dereg.assert_not_called() + + +def test_deregister_is_idempotent_after_register(): + buf = Buffer(0x1000, 2048, 0) + with mock.patch.object( + hipfile.buffer, "hipFileBufRegister", autospec=True, return_value=_SUCCESS + ): + buf.register() + with mock.patch.object( + hipfile.buffer, "hipFileBufDeregister", autospec=True, return_value=_SUCCESS + ) as dereg: + buf.deregister() + buf.deregister() + # Second call is a no-op: the extension is only invoked once. + dereg.assert_called_once_with(0x1000) + + +def test_deregister_raises_on_error(): + buf = Buffer(0x1000, 2048, 0) + with mock.patch.object( + hipfile.buffer, "hipFileBufRegister", autospec=True, return_value=_SUCCESS + ): + buf.register() + with mock.patch.object( + hipfile.buffer, "hipFileBufDeregister", autospec=True, return_value=_FAILURE + ): + with pytest.raises(HipFileException): + buf.deregister() + + +def test_context_manager_registers_then_deregisters(): + buf = Buffer(0x1000, 2048, 0) + with mock.patch.object( + hipfile.buffer, "hipFileBufRegister", autospec=True, return_value=_SUCCESS + ) as reg, mock.patch.object( + hipfile.buffer, "hipFileBufDeregister", autospec=True, return_value=_SUCCESS + ) as dereg: + with buf as entered: + assert entered is buf + reg.assert_called_once_with(0x1000, 2048, 0) + dereg.assert_not_called() + dereg.assert_called_once_with(0x1000) diff --git a/projects/hipfile/python/tests/test_driver.py b/projects/hipfile/python/tests/test_driver.py new file mode 100644 index 00000000000..b903c723e81 --- /dev/null +++ b/projects/hipfile/python/tests/test_driver.py @@ -0,0 +1,74 @@ +# Copyright (c) Advanced Micro Devices, Inc. All rights reserved. +# +# SPDX-License-Identifier: MIT + +"""Tests for hipfile.driver.Driver.""" + +# pylint: disable=missing-function-docstring,redefined-outer-name +# pylint: disable=import-error # hipfile not installed in lint env; extension faked + +from unittest import mock + +import pytest + +import hipfile.driver +from hipfile.driver import Driver +from hipfile.enums import OpError +from hipfile.error import HipFileException + +_SUCCESS = (OpError.SUCCESS, 0) +_FAILURE = (OpError.DRIVER_NOT_INITIALIZED, 0) + + +def test_open_success(): + with mock.patch.object( + hipfile.driver, "hipFileDriverOpen", autospec=True, return_value=_SUCCESS + ) as drv_open: + Driver().open() + drv_open.assert_called_once_with() + + +def test_open_raises_on_error(): + with mock.patch.object( + hipfile.driver, "hipFileDriverOpen", autospec=True, return_value=_FAILURE + ): + with pytest.raises(HipFileException) as excinfo: + Driver().open() + assert excinfo.value.hipfile_err == OpError.DRIVER_NOT_INITIALIZED + + +def test_close_success(): + with mock.patch.object( + hipfile.driver, "hipFileDriverClose", autospec=True, return_value=_SUCCESS + ) as drv_close: + Driver().close() + drv_close.assert_called_once_with() + + +def test_close_raises_on_error(): + with mock.patch.object( + hipfile.driver, "hipFileDriverClose", autospec=True, return_value=_FAILURE + ): + with pytest.raises(HipFileException): + Driver().close() + + +def test_use_count_delegates(): + with mock.patch.object( + hipfile.driver, "hipFileUseCount", autospec=True, return_value=5 + ) as use_count: + assert Driver.use_count() == 5 + use_count.assert_called_once_with() + + +def test_context_manager_opens_then_closes(): + with mock.patch.object( + hipfile.driver, "hipFileDriverOpen", autospec=True, return_value=_SUCCESS + ) as drv_open, mock.patch.object( + hipfile.driver, "hipFileDriverClose", autospec=True, return_value=_SUCCESS + ) as drv_close: + with Driver() as drv: + assert isinstance(drv, Driver) + drv_open.assert_called_once_with() + drv_close.assert_not_called() + drv_close.assert_called_once_with() diff --git a/projects/hipfile/python/tests/test_enums.py b/projects/hipfile/python/tests/test_enums.py new file mode 100644 index 00000000000..b108649536f --- /dev/null +++ b/projects/hipfile/python/tests/test_enums.py @@ -0,0 +1,17 @@ +# Copyright (c) Advanced Micro Devices, Inc. All rights reserved. +# +# SPDX-License-Identifier: MIT + +"""Tests for hipfile.enums""" + +# pylint: disable=missing-function-docstring +# pylint: disable=import-error # hipfile not installed in lint env; extension faked + +from hipfile.enums import OpError + + +def test_op_error_members_are_ints(): + # The high-level code compares error codes against raw ints returned by the + # extension (e.g. ``err[0] != 0``), so members must behave as ints. + assert isinstance(OpError.SUCCESS, int) + assert OpError.SUCCESS == 0 diff --git a/projects/hipfile/python/tests/test_error.py b/projects/hipfile/python/tests/test_error.py new file mode 100644 index 00000000000..c27ed4b2254 --- /dev/null +++ b/projects/hipfile/python/tests/test_error.py @@ -0,0 +1,35 @@ +# Copyright (c) Advanced Micro Devices, Inc. All rights reserved. +# +# SPDX-License-Identifier: MIT + +"""Tests for hipfile.error.HipFileException.""" + +# pylint: disable=missing-function-docstring,redefined-outer-name +# pylint: disable=import-error # hipfile not installed in lint env; extension faked + +from unittest import mock + +import hipfile.error +from hipfile.enums import OpError +from hipfile.error import HipFileException + + +def test_stores_error_codes(): + exc = HipFileException(OpError.INVALID_VALUE, 7) + assert exc.hipfile_err == OpError.INVALID_VALUE + assert exc.hip_err == 7 + + +def test_str_includes_op_error_string(): + with mock.patch.object( + hipfile.error, "hipFileGetOpErrorString", return_value="boom" + ) as get_str: + exc = HipFileException(OpError.INVALID_VALUE, 0) + text = str(exc) + get_str.assert_called_once_with(OpError.INVALID_VALUE) + assert str(int(OpError.INVALID_VALUE)) in text + assert "boom" in text + + +def test_is_an_exception(): + assert issubclass(HipFileException, Exception) diff --git a/projects/hipfile/python/tests/test_file.py b/projects/hipfile/python/tests/test_file.py new file mode 100644 index 00000000000..ae305b9792f --- /dev/null +++ b/projects/hipfile/python/tests/test_file.py @@ -0,0 +1,197 @@ +# Copyright (c) Advanced Micro Devices, Inc. All rights reserved. +# +# SPDX-License-Identifier: MIT + +"""Tests for hipfile.file.FileHandle.""" + +# pylint: disable=missing-function-docstring,redefined-outer-name,protected-access +# pylint: disable=import-error # hipfile not installed in lint env; extension faked + +import errno +import os +from unittest import mock + +import pytest + +import hipfile.file +from hipfile.enums import FileHandleType, OpError +from hipfile.error import HipFileException +from hipfile.file import FileHandle + +_SUCCESS = (OpError.SUCCESS, 0) +_FAILURE = (OpError.HANDLE_ALREADY_REGISTERED, 0) +_FAKE_FD = 11 +_FAKE_HANDLE = 0xF11E + + +# --- handle_type setter guards -------------------------------------------- + + +def test_handle_type_rejects_non_member(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + with pytest.raises(ValueError): + fh.handle_type = 9999 + + +def test_handle_type_rejects_win32(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + with pytest.raises(NotImplementedError): + fh.handle_type = FileHandleType.OPAQUE_WIN32 + + +def test_handle_type_rejects_change_while_open(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + fh._handle = _FAKE_HANDLE # simulate an open handle + with pytest.raises(RuntimeError): + fh.handle_type = FileHandleType.USERSPACE_FS + + +# --- open ----------------------------------------------------------------- + + +def test_open_registers_and_stores_handle(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + with mock.patch.object( + hipfile.file.os, "open", return_value=_FAKE_FD + ), mock.patch.object( + hipfile.file, + "hipFileHandleRegister", + autospec=True, + return_value=(_FAKE_HANDLE, _SUCCESS), + ) as register: + fh.open() + assert fh.handle == _FAKE_HANDLE + register.assert_called_once_with(_FAKE_FD, FileHandleType.OPAQUE_FD) + + +def test_open_closes_fd_when_registration_fails(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + with mock.patch.object( + hipfile.file.os, "open", return_value=_FAKE_FD + ), mock.patch.object(hipfile.file.os, "close") as os_close, mock.patch.object( + hipfile.file, + "hipFileHandleRegister", + autospec=True, + return_value=(0, _FAILURE), + ): + with pytest.raises(HipFileException): + fh.open() + os_close.assert_called_once_with(_FAKE_FD) + assert fh.handle is None + + +def test_open_raises_when_already_open(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + fh._handle = _FAKE_HANDLE + with pytest.raises(RuntimeError): + fh.open() + + +# --- close ---------------------------------------------------------------- + + +def test_close_deregisters_and_closes(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + fh._handle = _FAKE_HANDLE + fh._fd = _FAKE_FD + with mock.patch.object( + hipfile.file, "hipFileHandleDeregister", autospec=True + ) as dereg, mock.patch.object(hipfile.file.os, "close") as os_close: + fh.close() + dereg.assert_called_once_with(_FAKE_HANDLE) + os_close.assert_called_once_with(_FAKE_FD) + assert fh.handle is None + assert fh._fd is None + + +def test_close_on_never_opened_is_noop(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + with mock.patch.object( + hipfile.file, "hipFileHandleDeregister", autospec=True + ) as dereg, mock.patch.object(hipfile.file.os, "close") as os_close: + fh.close() + dereg.assert_not_called() + os_close.assert_not_called() + + +def test_close_is_idempotent_after_open(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + fh._handle = _FAKE_HANDLE + fh._fd = _FAKE_FD + with mock.patch.object( + hipfile.file, "hipFileHandleDeregister", autospec=True + ) as dereg, mock.patch.object(hipfile.file.os, "close") as os_close: + fh.close() + fh.close() + # Cleanup runs exactly once despite the repeated call. + dereg.assert_called_once_with(_FAKE_HANDLE) + os_close.assert_called_once_with(_FAKE_FD) + + +# --- read / write --------------------------------------------------------- + + +def _open_handle(): + fh = FileHandle("/tmp/x", os.O_RDONLY) + fh._handle = _FAKE_HANDLE + return fh + + +@pytest.mark.parametrize("method", ["read", "write"]) +def test_io_raises_when_not_open(method, fake_buffer): + fh = FileHandle("/tmp/x", os.O_RDONLY) + with pytest.raises(RuntimeError): + getattr(fh, method)(fake_buffer, 64, 0, 0) + + +@pytest.mark.parametrize("method", ["read", "write"]) +def test_io_success_returns_bytecount(method, fake_buffer): + fh = _open_handle() + wrapper = "hipFileRead" if method == "read" else "hipFileWrite" + with mock.patch.object( + hipfile.file, wrapper, autospec=True, return_value=(64, 0) + ) as io_call: + result = getattr(fh, method)(fake_buffer, 64, 8, 16) + assert result == 64 + io_call.assert_called_once_with(_FAKE_HANDLE, fake_buffer.ptr, 64, 8, 16) + + +@pytest.mark.parametrize("method", ["read", "write"]) +def test_io_system_error_raises_oserror(method, fake_buffer): + fh = _open_handle() + wrapper = "hipFileRead" if method == "read" else "hipFileWrite" + with mock.patch.object( + hipfile.file, wrapper, autospec=True, return_value=(-1, errno.EIO) + ): + with pytest.raises(OSError) as excinfo: + getattr(fh, method)(fake_buffer, 64, 0, 0) + assert excinfo.value.errno == errno.EIO + + +@pytest.mark.parametrize("method", ["read", "write"]) +def test_io_hipfile_error_raises_exception(method, fake_buffer): + fh = _open_handle() + wrapper = "hipFileRead" if method == "read" else "hipFileWrite" + # result < -1 encodes a negated hipFileOpError_t with extra_err == 0. + op_err = OpError.INVALID_VALUE + with mock.patch.object( + hipfile.file, wrapper, autospec=True, return_value=(-int(op_err), 0) + ): + with pytest.raises(HipFileException) as excinfo: + getattr(fh, method)(fake_buffer, 64, 0, 0) + assert excinfo.value.hipfile_err == op_err + assert excinfo.value.hip_err == 0 + + +@pytest.mark.parametrize("method", ["read", "write"]) +def test_io_hip_driver_error_carries_hip_err(method, fake_buffer): + fh = _open_handle() + wrapper = "hipFileRead" if method == "read" else "hipFileWrite" + op_err = OpError.HIP_DRIVER_ERROR + with mock.patch.object( + hipfile.file, wrapper, autospec=True, return_value=(-int(op_err), 99) + ): + with pytest.raises(HipFileException) as excinfo: + getattr(fh, method)(fake_buffer, 64, 0, 0) + assert excinfo.value.hipfile_err == op_err + assert excinfo.value.hip_err == 99 diff --git a/projects/hipfile/python/tests/test_properties.py b/projects/hipfile/python/tests/test_properties.py new file mode 100644 index 00000000000..bcf0bfe76cc --- /dev/null +++ b/projects/hipfile/python/tests/test_properties.py @@ -0,0 +1,67 @@ +# Copyright (c) Advanced Micro Devices, Inc. All rights reserved. +# +# SPDX-License-Identifier: MIT + +"""Tests for hipfile.properties: get_version and driver_get_properties.""" + +# pylint: disable=missing-function-docstring,redefined-outer-name +# pylint: disable=import-error # hipfile not installed in lint env; extension faked + +from unittest import mock + +import pytest + +import hipfile.properties +from hipfile.enums import OpError +from hipfile.error import HipFileException +from hipfile.properties import driver_get_properties, get_version + +_SUCCESS = (OpError.SUCCESS, 0) +_FAILURE = (OpError.DRIVER_NOT_INITIALIZED, 0) + + +def test_get_version_success(): + with mock.patch.object( + hipfile.properties, + "hipFileGetVersion", + autospec=True, + return_value=((4, 5, 6), _SUCCESS), + ) as get_ver: + assert get_version() == (4, 5, 6) + get_ver.assert_called_once_with() + + +def test_get_version_raises_on_error(): + with mock.patch.object( + hipfile.properties, + "hipFileGetVersion", + autospec=True, + return_value=((0, 0, 0), _FAILURE), + ): + with pytest.raises(HipFileException): + get_version() + + +# Note that test properties are mocked and may not reflect all properties +# that are tracked by the C library. +def test_driver_get_properties_success(): + props = {"feature_flags": 3, "max_batch_io_count": 7} + with mock.patch.object( + hipfile.properties, + "hipFileDriverGetProperties", + autospec=True, + return_value=(props, _SUCCESS), + ) as get_props: + assert driver_get_properties() == props + get_props.assert_called_once_with() + + +def test_driver_get_properties_raises_on_error(): + with mock.patch.object( + hipfile.properties, + "hipFileDriverGetProperties", + autospec=True, + return_value=({}, _FAILURE), + ): + with pytest.raises(HipFileException): + driver_get_properties()